feat(grid-agent): isolate conversation memory (#122)
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m48s
CI / required (push) Failing after 2m42s

This commit is contained in:
2026-08-17 22:18:24 +00:00
parent 3553c83ffa
commit e3ed39471b
10 changed files with 2173 additions and 5 deletions

View File

@@ -27,6 +27,7 @@ observable receivers.
| Offline session work | live supervisor | 1,024 hard / `reconnect.offline_work_capacity` | read-only queue; mutations rejected while not ready |
| Body / message | typed boundary owners | 8 MiB / 64 KiB hard ceilings, with lower configured limits | rejected before enqueue |
| Conversation / tool calls | request owner | 256 messages / 64 calls, with lower configured limits | rejected before request |
| Per-avatar conversation memory | `ConversationStore` mutex | 4,096 sessions / 64 MiB hard, lower `conversation` limits | monotonic expiry, deterministic compaction/LRU eviction |
| LLM request slots | shared `LlmClient` semaphore | 256 hard / configured concurrent requests | async acquire or cancellation |
| Reasoning/tool session | `ToolLoop` caller | 32 turns / 256 calls hard, with lower configured limits | total timeout, cancellation, or supersession |
| Policy tools / approvals / schedules | `PolicyGateway` mutex | 64 tools / 4,096 approval records / 1,024 scheduler grants hard | deny before opaque authorization |
@@ -84,6 +85,10 @@ cleanup, fencing old events and late LLM/tool results. See
are refused, response bodies are bounded while streaming, bearer secrets are
redacted, and provider/model discovery does not exist. Proposed calls cross
`ToolExecutor` only after registered-name and schema validation.
- Conversation context is keyed by immutable avatar UUID and either public chat
or direct IM. Group channels are not representable. The LLM projection can
retrieve only one exact key, and recovered/untrusted summaries remain user-role
prompt data rather than system authority.
- Signals and console output belong to the binary. The reusable core relies on
no terminal, Unix socket, Unix signal, separator, or fixed platform path.
@@ -116,3 +121,5 @@ The origin/capability matrix and opaque mutation boundary are documented in
[`grid-agent-policy.md`](grid-agent-policy.md).
The live lifecycle and generation contract is documented in
[`grid-agent-session.md`](grid-agent-session.md).
The conversation isolation and persistence contract is documented in
[`grid-agent-conversation.md`](grid-agent-conversation.md).

View File

@@ -0,0 +1,55 @@
# Grid-agent conversation memory
`ConversationStore` owns bounded conversational context independently of the
grid connection generation. Its key is the immutable avatar UUID plus exactly
one channel kind: public chat or direct IM. Group and conference conversations
are deliberately not representable. Public sessions expire at exactly 30
minutes of monotonic inactivity and direct IM sessions at exactly 24 hours. A
subsequent turn creates a cryptographically random new session ID and receives
none of the expired transcript.
The mutex-protected store assigns one sequence order to concurrent turns. It
stores wall-clock timestamps, normalized avatar/agent/tool roles, visible agent
responses, bounded tool summaries, and externally meaningful action results.
Avatar input and untrusted tool data remain untrusted when compacted. There is
no system-message, model-scratchpad, hidden-reasoning, credential, capability
URL, or binary-asset input variant. Text is bounded and sensitive URL/token
forms are redacted before allocation in the store.
Configured limits lower hard ceilings for active sessions, turns, per-session
bytes, aggregate bytes, tool-result count and size, summary bytes, and total
snapshot storage. Old records compact deterministically into a smaller factual
summary followed by recent context. Session and aggregate pressure evict the
least-recently-active key with UUID/channel tie-breaking. Expiry, compaction,
eviction, quarantine, and operator deletion publish stable reason codes through
a bounded event queue.
`list_metadata` returns session ID, UUID, channel, timestamps, turn count, and
byte count but never content. Operators can delete or expire an exact key.
`context` is the separate LLM-facing projection and can read only that key;
untrusted summaries are emitted as explicitly marked user-role data.
Persistence is local and opt-in through `conversation.persistence_enabled`.
`ConversationStore::from_config` uses `storage_path/conversations`; callers
flush at their durability boundary. A flush writes and syncs a new immutable,
versioned generation before an atomic rename, then retains only generations
whose aggregate bytes fit the configured storage ceiling. Linux and other Unix
targets force directory mode 0700 and file mode 0600. Rust standard library
does not expose a portable Windows ACL editor, so Windows emits the explicit
`PermissionsNotVerified` event and operators must restrict the directory ACL to
the service identity.
Restart recovery validates schema, UUIDs, IDs, unique sequences, timestamps,
roles, bounds, and redaction before any record can become LLM context. A
truncated, corrupt, oversized, or unsupported generation is quarantined and an
older valid generation is tried. If the wall clock moved backwards, recovered
age is zero; forward elapsed time is applied to the channel TTL. Neither case
can grant policy authority or prevent startup.
Focused gates:
```sh
cargo test --locked -p metacrate-grid-agent --lib conversation_tests
cargo test --locked -p metacrate-grid-agent --test conversation_memory
cargo clippy --locked -p metacrate-grid-agent --all-targets -- -D warnings
```