Session keys (sessionKey)

A sessionKey identifies which conversation bucket you are in (routing + isolation). Canonical rules: /concepts/session.

Pattern Example
Main/direct chat (per agent) agent:<agentId>:main
Group agent:<agentId>:<channel>:group:<id>
Room/channel (Discord/Slack) agent:<agentId>:<channel>:channel:<id> or ...:room:<id>
Cron cron:<job.id>
Webhook hook:<uuid> (unless overridden)

Session ids (sessionId)

Each sessionKey points at a current sessionId (the SQLite transcript identity that continues the conversation). Decision logic lives in initSessionState() in src/messaging/auto-reply/reply/session.ts.

Session store schema

The runtime store keeps SessionEntry values in per-agent SQLite. The value type is SessionEntry in src/platform/config/sessions.ts. Key fields (not exhaustive):

Existing label-only records are preserved: the Gateway does not infer whether a saved label was automatic from its text. Clear or replace it explicitly to change its precedence. An older Gateway that does not support autoLabel rejects that patch field; Android does not retry the device name as label, which could overwrite a custom name. Android still uses its locally known device name as a display-only fallback for its own session, below server-provided names. This fallback is neither sent to the Gateway nor stored in the session cache.

The Gateway is the authority: it may rewrite or rehydrate entries as sessions run. For legacy file-backed installs, migrate with apus doctor --session-sqlite import --session-sqlite-all-agents instead of editing sessions.json and expecting runtime to keep reading that file.

Transcript event structure

Transcripts are managed by the Apus session accessor and exposed to runtime code through identity-based helpers. The event stream is append-only:

Notable entry types:

History readers keep the latest reset window across later compactions: explicitly retained reset messages and messages after that reset remain visible, but older messages and compaction summaries do not reappear. Model context follows the latest reset or compaction instead, so compaction can summarize the current conversation without reopening its earlier history.

Model-only callers should await SessionManager.openModelContextAsync(target, { admission?, signal? }) to create a detached, non-persisting view without blocking the Gateway event loop on durable history scans. openModelContext() provides the same view for synchronous consumers. The reader selects payloads in SQLite and retains lightweight navigation outside the model window, without introducing a history size cutoff. Storage-only native prompt text and tool-result details stay out of that view; mirror identity, sender and media facts, tool content, and valid provider replay state remain available. Native fork verification, replay, exports, and doctor operations continue to use full-fidelity evidence readers.

SessionManager.readSessionContext(target, read, { admission? }) lets a synchronous consumer process full-fidelity context messages inside one read-only snapshot. The callback receives (messages, header): a lazy message iterable and the unvalidated stored header. Missing stores supply an empty iterable and no header without creating a database. An optional admission excludes the current admitted user row and later events. Callback errors propagate, and the iterator closes when the callback returns or throws; it cannot be retained for later reads. This lets replay consumers enforce their existing limits during acquisition without silently dropping earlier history. Navigation still scales with the transcript, and individual selected rows are decoded whole; this is not a fixed process-memory ceiling.

Durable model-context reads run in a worker. Codex native replay and settled-turn verification keep that lazy read and its consumer together in a worker. Worker reads are serialized per reader and reuse an idle worker. Admission receipts are validated inside the read snapshot and again before the result is accepted; reads without an admission instead check the session’s rewrite generation and last event sequence. Admitted reads retain their turn boundary, so later appends alone do not invalidate them. An invalidated read or canceled signal rejects the result. Callers carry their original cancellation signal through context acquisition and check that their owner remains active before invoking hooks, starting a model run, or applying a proposal. Incognito sessions use the same operation in the Gateway process because their SQLite database is held in memory.

Apus intentionally does not "fix up" transcripts; the Gateway uses SessionManager to read/write them.