Skip to content

Memory, learning and knowledge settings ​

These settings are read from config.json. Examples are partial objects to merge into an existing configuration. Defaults in the tables refer to runtime fallback values; the installed template may explicitly choose a different value.

Memory manager settings and timers refresh through the configuration change lifecycle. In-flight dreaming/reflection jobs retain their resolved settings; skill-learning reconfiguration waits for active reviews. A schedule change applies to future runs rather than triggering a job immediately.

gateway.skillLearning ​

Controls skill learning — agents learning reusable skills from their own work. Telemetry capture is always on; the reviewer/writer/curator honor enabled.

FieldDefaultDescription
enabledtrueMaster switch for the reviewer/writer/curator (telemetry is captured regardless)
mode"auto"auto writes skills directly; propose queues them for approval instead
minToolCalls5Minimum tool calls in a turn before it's eligible for review
reviewModelclaude-haiku-4-5-…Model used for the background review pass
maxAutoSkills20Cap on the number of non-pinned origin: auto skills kept per agent (pinned skills are never evicted and don't count toward the cap)
maxAgeDays30Curator prunes auto-skills older than this (with too few uses)
minUsesToKeep2Auto-skills used fewer times than this are prune candidates
maxReviewsPerDay20Per-day cap on background review runs
pruneHour / pruneTimezone3 / UTCWhen the daily curator runs; pruneTimezone falls back to gateway.timezone when unset or invalid
notifytrueNotify only the originating session (see skill notifications); the SKILLS_LEARNED.md diary is written regardless
json
{
  "gateway": {
    "skillLearning": {
      "enabled": true,
      "mode": "auto",
      "notify": true
    }
  }
}

Per-agent overrides are supported under the agent's own skillLearning block; unset fields fall back to the gateway default.

gateway.memory ​

Memory budget discipline. Self-authored memory files (MEMORY.md, USER.md) that exceed a soft char budget get a loud over-budget banner prepended to their CLAUDE.md section at compose time — instead of a silent [TRUNCATED] — nudging the agent to consolidate. The banner reaches the agent on its next spawn (frozen-at-spawn, no restart) and self-heals once the file is back under budget. The banner lives only in the composed CLAUDE.md; the source file on disk is never rewritten with it.

FieldDefaultDescription
memoryBudgetChars8000Soft budget for MEMORY.md (0 = disabled)
userBudgetChars3000Soft budget for USER.md (0 = disabled)
overBudget"warn"Banner severity: warn (⚠️) or error (🛑, stronger wording); an unknown value falls back to warn
writeRoutingfalse (template: true)Inject the two-tier write contract into the Memory Rule (MEMORY.md = durable facts; task-log → memory/<topic>.md) and let nightly dreaming route episodic ops out. false = kill-switch (exact pre-routing behavior)
episodicArchiveDir"memory"Workspace-relative dir episodic notes are written under (validated, path-traversal-guarded)
json
{
  "gateway": {
    "memory": {
      "memoryBudgetChars": 8000,
      "userBudgetChars": 3000,
      "overBudget": "warn",
      "writeRouting": true,
      "episodicArchiveDir": "memory"
    }
  }
}

The soft budget sits well under the hard per-file limit (still applied as a context safety net); the banner is the primary over-budget signal for memory files.

Write routing (planning-65). MEMORY.md is injected into every prompt, so it should hold only durable semantic facts (preferences, standing rules, identity, lessons). Episodic task-log (completed work, PR/issue status, dated events) belongs in memory/<topic>.md — indexed and retrieved on demand via memory_search, never carried in-prompt. When writeRouting is on, the Memory Rule states this tier contract to the agent, and the nightly dreaming reviewer may emit tier:"episodic" ops that the applier appends to memory/<topic>.md (slug-validated + realpath-confined; a memory-only change ⇒ no session restart). To drain an existing over-budget MEMORY.md, run the one-shot migration node dist/agent/dreaming/migrate-cli.js <workspaceDir> [--apply] — a deterministic terminal sweep (compactor) plus a gated episodic route-out (propose writes .dreaming/migration-plan.md; --apply performs the moves). Pinned sections (## User, ## Feedback, ## Preferences) are never moved, and every relocated entry stays searchable via memory_search (recall preserved). planning-67: with gateway.dreaming.autoRouteOut on (the default), the nightly dream performs this same route-out automatically whenever MEMORY.md is over budget — no manual per-agent run — and every over-budget net-shrink remove now relocates its block to memory/archive/pruned.md (searchable) before cutting it, so no dream op ever silently forgets.

gateway.dreaming ​

Nightly memory dreaming — background consolidation of an agent's long-term memory. A print-only claude -p reviewer (no tools, no --dangerously-skip-permissions) reads a lookback window of the agent's own session transcripts and proposes memory-consolidation ops. In auto mode (the default) a safe applier writes the ops to MEMORY.md/USER.md (rollback pre-image first; ordered apply with anchor re-resolution; bounded-loss + append-only fallback; net-negative when over budget) — a memory-only change, so no session is restarted. In propose mode the proposals are written only to a DREAMS.md diary + JSONL audit under <workspace>/.dreaming/ — no memory file is modified (set mode: "propose" to keep this dry-run behavior).

FieldDefaultDescription
enabledtrueMaster switch (false ⇒ no scheduler, no run)
mode"auto"auto = apply ops via the safe applier (backup, bounded-loss, net-negative); propose = diary-only dry-run
dreamHour / dreamTimezone3 / UTCWhen the nightly dream runs (invalid tz → gateway.timezone, then UTC); dreamTimezone falls back to gateway.timezone when unset or invalid
dreamMinute0Minute-of-hour the dream fires at, paired with dreamHour (0–59). Set with staggerWindowMinutes: 0 to fire at an exact HH:MM (e.g. for a controlled re-test)
quietMinutes30Skip a run if a session was active within this window
lookbackDays3How far back to scan sessions
maxChangesPerRun3Cap on proposed ops per run (0 ⇒ no-op)
reviewModelclaude-haiku-4-5-…Cheap model for the reviewer
promotionThreshold / minRecallCount0.6 / 2Scoring thresholds for promoting a fact
autoRouteOuttrueplanning-67: in auto mode, drain an over-budget MEMORY.md by routing its episodic task-log to memory/<topic>.md automatically each night (archive-safe, pinned excluded, idempotent) instead of a manual per-agent migrate-cli. false = kill-switch
staggerWindowMinutes30planning-68: spread agents' nightly runs across a window (a deterministic per-agent jitter is added to the delay) so they don't all fire at dreamHour:00 together. Clamped [0,55]; 0 = disabled (all fire at dreamHour:00)
staleness(object)Archive staleness GC sub-config (planning-66) — see below

Per-agent overrides are supported under the agent's own dreaming block; unset fields fall back to the gateway default. enabled:false or maxChangesPerRun:0 makes a run a no-op.

⚠️ Upgrade note: the default mode for both gateway.dreaming and gateway.knowledge.shared changed from propose (dry-run) to auto (configVersion 1.0.24). Once the K4 applier landed (backup + net-negative + bounded-loss + CAS + never-empty; memory-only write ⇒ no session restart), auto became the intended default: nightly dreaming now applies consolidation to MEMORY.md/USER.md and promotes durable memories to the shared vault. Like the gateway.bind migration, the migrator upgrades the retired propose default to auto once and logs a one-time warning; an explicit mode you set at 1.0.24+ is never touched. To keep dry-run, set mode: "propose" explicitly.

Keeping MEMORY.md near budget (auto mode). Two mechanisms stop the on-disk MEMORY.md from growing unbounded while preserving recall:

  • Deterministic compaction — before the LLM reviewer, every auto run moves completed/terminal log entries out of MEMORY.md into memory/archive/completed.md, leaving a one-line pointer. It is domain-agnostic (not just dev): an entry is archived when its lead line carries an explicit done marker — an UPPERCASE status word (DONE, COMPLETED, RESOLVED, CLOSED, CANCELLED, ARCHIVED, MERGED, SUPERSEDED, OBSOLETE, DEPRECATED, EXPIRED, SHIPPED, FINISHED), a checked task box [x], a ✅, or a strikethrough — and it works on both list bullets and ### entry headers. The archive lives under memory/ so it is still indexed and searchable via memory_search — the agent recalls completed work on demand instead of carrying its full changelog in-prompt. It is conservative (uppercase words only, so prose like "Closes #123", "we're not done", or an unchecked [ ] box is never archived), idempotent, and only moves entries whose lead line matches a terminal marker, preserving their original text in the archive.
  • Budget-scaled pruning — when MEMORY.md is over its soft budget, the reviewer is put in an explicit net-shrink mode (propose only length-reducing ops) and maxChangesPerRun scales up for removals (the add cap stays tight), so an over-budget file converges toward budget instead of trickling at a few edits per night.
  • Archive staleness GC (gateway.dreaming.staleness, planning-66) — a deterministic pass that runs next to the compactor (auto mode) to keep the Lane-2 archive's search quality high. This is a search-quality fix, not a prompt-budget one: planning-65 already moved task-log off the injected prompt, so the point here is that memory_search should keep surfacing current truth instead of stale/superseded facts. Each nightly run soft-invalidates archive entries — superseded ones (a deterministic supersedes/replaces/obsoletes #N match, which finally populates the previously-inert supersedes_key) and aged-out ones (idle-since-last-retrieval past staleTtlDays and retrieved fewer than minRetrievalKeep times) — by moving them to memory/archive/stale.md and stamping invalid_at. It never deletes: a staled entry stays under memory/ so it is still indexed and searchable . An entry that is retrieved after it was invalidated is promoted back to the active archive (the recall feedback loop — proof we aged it out too soon). Recall is fed by an append-only read-path log (kb_retrieval_log, gated by recordRetrievals) that the GC folds into each entry's last_retrieved. High-importance entries (keepImportance) and pinned files (memory/pinned/**) are never aged out; evergreen Lane-1 (MEMORY.md/USER.md) is structurally excluded. Every move is CAS-guarded with a timestamped backup, and — being a memory-only write — drops no live session. One run may soft-invalidate at most staleness.maxInvalidationsPerRun entries (default 50), oldest-idle first, with the remainder resuming on later runs — aging is wall-clock driven, so without a ceiling the first run after anything that widens the GC's visibility (such as backfilling lifecycle rows for previously invisible sources) would relocate every already-expired entry in one night. Restores are never capped. Kill-switches: staleness.enabled:false (GC no-ops), maxInvalidationsPerRun:0 (never invalidates, still restores) and recordRetrievals:false (age falls back to first-seen only).

gateway.knowledge ​

Two-lane memory — a per-agent searchable knowledge archive so an agent can recall what does not fit in the always-injected core. A SQLite/FTS5 index (agents/<id>/kb.sqlite, built on Node's built-in node:sqlite — no new dependency) covers the agent's memory/*.md notes plus the evergreen MEMORY.md/USER.md. Every chunk is tagged with fail-closed provenance (owner/agent/untrusted/system; unclassified ⇒ untrusted). The index is refreshed by a detached subprocess at session spawn, entirely off the gateway event loop.

Two read-only MCP tools expose it to the agent: memory_search (keyword/FTS5 → ranked snippets with file+line, provenance, importance) and memory_get (bounded, path-traversal-guarded excerpt of a memory-scoped file). When MEMORY.md grows past its gateway.memory soft budget, compose injects a compact auto-generated section index + a pointer to memory_search instead of the truncated full text (core-shrink) — the on-disk file is never modified and its full content stays searchable. Whenever the archive is on, a short --- MEMORY RETRIEVAL --- note is also injected into every agent's system prompt so the tools stay discoverable at all times (not only when the file is over budget).

FieldDefaultDescription
archive.enabledtrueMaster switch (false ⇒ complete no-op, no DB created, no core-shrink)
archive.tokenizer"unicode61"FTS5 tokenizer ("trigram" for CJK/Thai)
archive.chunkTokens400Target chunk size in ~tokens
archive.chunkOverlap80Overlap between chunks (clamped below chunkTokens)
shared.enabledtrueEnable the cross-agent shared KB
shared.project"global"Sharing partition key (one safe path segment) — agents with the same value share one vault; "global" ⇒ shared-by-default
shared.root~/.claude-gateway/shared/kbShared vault root dir (<root>/<project>/)
shared.mode"auto"Per-agent→shared promotion mode; auto = promote durable dreamed facts, propose = dry-run
shared.graphfalseCompile the memory-wiki graph + dashboards over the shared vault to <vault>/reports/*.md (opt-in). Independent of the dashboard Knowledge base tab, which computes its graph on-demand
shared.staleness(object)Shared-note TTL lifecycle GC; uses the same fields/defaults as dreaming.staleness (whole notes only; no numeric supersedes #N syntax)
reflection.enabledtrueEnable the singleton, per-shared-vault reflection scheduler (daily timer; see cadence note below)
reflection.dayOfWeek / hour / minute / timezone0 / 4 / 0 / UTChour/minute is the daily staleness-GC slot; dayOfWeek selects the weekday that additionally runs LLM consolidation (Sunday 04:00 UTC by default; invalid timezone falls back to gateway.timezone, then UTC)
reflection.maxClustersPerRun / reviewModel5 / claude-haiku-4-5-…Hard cap on changed linked-note clusters per consolidation run and the bounded synthesis model

Shared KB. A shared SQLite/FTS5 vault outside any single agent's workspace lets agents build a common knowledge base. Notes under <root>/<project>/notes/*.md are indexed and reachable via memory_search with corpus:"shared" (the shared vault) or corpus:"all" (this agent's memory + shared, merged by relevance). Concurrent writers are safe without a lock — atomic note writes (temp+rename) plus a cross-process PRAGMA busy_timeout on the index. Per-agent overrides under the agent's own knowledge block. The MCP layer runs under Bun, so the read tools query kb.sqlite via bun:sqlite. Two write paths feed the vault, sharing one freeform-name namespace (issue #386, no agent-id prefix, no ownership scoping): the nightly dreaming promoter (gated by mode:"auto"; it promotes only content that carries a real fact — content that is nothing but MEMORY.md index-pointer bullets is skipped, since those links resolve only inside the promoting agent's own workspace — and names each note after the proposal's topic slug, so a recurring fact updates the same note across nights instead of piling up near-duplicates; a proposal without a valid topic is named after a reason that is itself a valid slug, else from the fact itself, never from prose reason text — the promotion is only abandoned when nothing nameable remains, and every skip is logged — including a write the note-size cap refuses and an unexpected write failure. A name that doesn't collide is checked against a near-duplicate search, but an unattended merge now also requires real token containment against the candidate — below that bar the fact gets its own note, since two notes are recoverable while two unrelated facts fused into one are not. [[wikilink]]s to related notes use a lower bar than merges, because a link is additive where a merge is destructive — and they are attached whether the fact merges or lands as a new note, so a note below the merge bar is never a disconnected graph node. Containment is scored against each candidate's full body rather than the matched chunk, though against a capped seed — the bar means "half of the fact's leading topic words are already here", not half of the whole fact. Retired stale__* notes are never merge targets; a recurrence of a retired name folds the retired body back in and removes the twin on both the create and the update path, because a retired note stays searchable and a twin beside a live note of the same name would answer every query twice forever. The twin is only dropped once the merged write lands (issue #398)) and the memory_shared_create/memory_shared_get/memory_shared_update/memory_shared_delete MCP tools, which let any agent create, read, update, or delete any note on demand regardless of mode. memory_shared_create warns instead of writing when it finds content-similar existing notes (pass confirm:true to proceed — related notes get [[wikilink]]ed into the new note rather than left disconnected); memory_shared_update warns instead of writing when the edit would drop 50%+ of the existing note's lines (same confirm:true escape hatch). Immediate reindex after every write or delete.

Shared lifecycle + reflection (issues #392, #398). Each shared note receives a stable whole-file lifecycle identity during indexing — including notes whose content has not changed since they were first indexed, which are backfilled from their source mtime so their real age is preserved. Its deterministic TTL GC runs daily, soft-invalidating aged low-recall notes by moving them to notes/stale__<name>.md (never deleting them from the searchable vault); a retrieval after invalidation restores the original active name. Shared memory_search and memory_shared_get reads feed the same append-only retrieval log as personal archive recall. The singleton reflection scheduler runs once per resolved shared-vault root, not once per agent, and fires daily at hour:minute (a fire that lands a hair early re-arms on the next day's slot rather than serving the same one twice): every fire runs the inexpensive TTL GC (no model call), while graph/LLM consolidation runs only on dayOfWeek — and even then is skipped when kb_index_state.revision has not changed since the prior consolidation. Weekly model spend is therefore unchanged, while a note that is retired and then retrieved returns to the active set within a day instead of up to a week. For changed vaults it clusters only active wikilink-connected notes deterministically, then makes at most reflection.maxClustersPerRun bounded reviewer calls to merge genuinely duplicate clusters; related-but-distinct notes remain merely linked.

Knowledge base viewer. The web dashboard's Knowledge base tab renders the shared vault as an Obsidian-style force-directed graph (nodes = notes sized by link degree and coloured by type; edges = [[wiki-links]]; contradicting claims and stale notes are flagged). It is fed by GET /knowledge/graph, which computes the model on-demand from the vault (no dependency on shared.graph or the nightly reindex). When the vault is empty it shows a clearly-labelled demo dataset (with a size selector for scale testing). A source selector switches the graph between the cross-agent Shared KB and any single agent's own Lane-2 memory (workspace/memory), a node search box filters the graph, and clicking a node opens its full note (fetched via GET /knowledge/note) rendered as Markdown below the graph.

Nightly dreaming viewer. A Nightly dreaming tab renders each agent's memory-consolidation audit trail (.dreaming/DREAMS.md + promotions.jsonl) as a newest-first timeline of runs — mode (propose/auto), outcome, the proposed/applied changes with scores + anchors, and per-run token/session counts — fed by GET /knowledge/dreams and filterable by agent. For a propose-mode run you can accept proposals directly from the tab: an Accept button per proposal (and Accept all per run) POSTs to POST /knowledge/dreams/apply, which applies the selected ops to MEMORY.md/USER.md through the same K4 safe applier auto mode uses (backup + bounded-loss + net-negative + CAS; memory-only ⇒ no restart) and — when the shared KB is auto — promotes applied adds to the shared vault. Accepts are idempotent (recorded to .dreaming/accepted.jsonl); applied proposals show ✓ and a proposal whose anchor has since drifted is safely skipped and stays pending for a later retry.

Implementation: config loader, configuration template.

Isolated memory mode ​

Isolated memory mode is for one agent that serves many unrelated senders, such as a public Telegram or LINE bot. Without it, USER.md and MEMORY.md hold one profile and one memory for the whole agent, so a fact written while one sender talks reaches every other sender's session.

Host agents and app agents have separate switches. Both are off by default, and turning one on never turns on the other.

Host agents: agents[].isolateMemory ​

The operator sets this per agent in the gateway config:

json
{ "agents": [{ "id": "shop-bot", "isolateMemory": true }] }

The value must be true or false; the config loader skips an agent with any other value.

On a host agent the mode isolates memory, tools and the API, but it is not an operating-system sandbox: the agent's Bash and file tools run on the host as the gateway user, so they can still open another conversation's directory or the gateway config. Use it for a bot whose senders you trust not to attack it, such as your own family's chats. For untrusted senders, use an app agent, which gets a sandbox per conversation.

App agents: services.agent.isolate_memory in app.yaml ​

The app declares it for its own agent in the app.yaml agent service declaration:

yaml
services:
  agent:
    path: ./agent
    name: shop-bot
    isolate_memory: true

At install, update, reconfigure and every gateway start, the gateway writes isolateMemory into the app agent's config entry from this declaration, and a gateway start admits the agent with the declared value. A release that removes the line turns the mode off again, and a hand edit of isolateMemory on an app agent is reset the same way. A gateway start resets only agents of running apps: a stopped app's agent keeps its config entry until a gateway start finds the app running. inspect_app and POST /api/v1/apps/inspect report it as agentDeclaration.isolateMemory. The gateway composes each conversation's memory on the host and gives the agent its files as /workspace/chats/… paths. Each conversation's chats/ directory is part of the agent workspace, which the app's update and backup/restore already carry.

A sandbox container per conversation. Without isolate mode every conversation of an app agent runs in the one app container. With it, each conversation runs in its own container, <app>-agent-c-<hash>, created from the app container's image, user, environment and networks, with every capability dropped and no-new-privileges. It has its own processes, /tmp and home directory, and mounts only:

  • read-only, the persona files (CLAUDE.md, AGENTS.md, SOUL.md, IDENTITY.md, HEARTBEAT.md) and the workspace directories that exist when it is created, except chats/, memory/, media/, dot directories and symlinks; of .claude/, only skills, commands, agents and settings.json. Any other root file, such as the root MEMORY.md/USER.md, kb.sqlite, SKILLS_LEARNED.md or a note an agent wrote before the mode was on, is not mounted;
  • chats/<this conversation>/, the only writable workspace path;
  • this conversation's media directory (media/<channel>-<chatId>, or media/api-<session> for an API session);
  • its own Claude Code directory (~/.claude), kept on the host at agents/<id>/sandbox-homes/<conversation>/, which holds its Claude Code transcripts;
  • the orchestration task bridge's socket directory, read-only.

So a conversation can read and write only its own memory, cannot change the persona or skills, and cannot see another conversation's files, transcripts, task tickets or processes. Claude, Codex and orchestration workers of that conversation all run there. The gateway removes a sandbox 30 minutes after its last session ends, recreates it when its mounts or the app image change, and removes all of an app's sandboxes when the app is updated or uninstalled. The gateway refuses to run a session in a container whose mounts are wider than this.

When it is on:

  • Per-conversation memory. Each conversation keeps its own files at <workspace>/chats/<channel>-<chatId>-<hash>/USER.md and MEMORY.md, and is shown its own files only. A group chat is one conversation, so its members share one memory. An API chat (chat_id) is one conversation across all of its sessions and every API key that uses it. A heartbeat runs in each live session with that conversation's own memory; only when no session is live does it run in a __heartbeat__ session, which keeps a memory of its own. A new conversation starts with empty files and never inherits the workspace-root USER.md/MEMORY.md, which may still hold data written before the mode was turned on. AGENTS.md, IDENTITY.md, SOUL.md and HEARTBEAT.md stay shared by every conversation.
  • Shared CLAUDE.md without sender data. The composed CLAUDE.md leaves out the user profile and long-term memory sections. Its memory rule tells the agent to write only to the current conversation's files. Each session (orchestration Agent, worker or classic channel session) receives its own conversation's files when it spawns.
  • Scoped memory writes. task_memory_append writes MEMORY.md (the default) or USER.md under the conversation's chats/ directory. It refuses memory/<topic>.md, because those notes would never be injected or searchable.
  • No archive or skill tools. memory_search, memory_get and the memory_shared_* tools are not offered, because the knowledge archive is one index over every conversation, and the archive is not reindexed at spawn. Core-shrink and the write-routing addendum are off for the same reason. skill_create, skill_install, skill_delete and skill_metrics are not offered either, because the skill catalog is shared by every conversation. The cron_*, agent_create and agent_update tools are off too: scheduled jobs and their prompts belong to the whole agent, and agent_update rewrites the shared persona files. So are the app tools (install_app, update_app, restore_app, uninstall_app and the rest), because updating, restoring or uninstalling an app rewinds or deletes the memory of every conversation its agent serves.
  • No Claude Code auto memory. Claude Code keeps one auto-memory directory per working directory (~/.claude/projects/<cwd>/memory/) and loads its index into every session there, so every conversation would share it. Sessions and workers of the agent run with CLAUDE_CODE_DISABLE_AUTO_MEMORY=1.
  • No nightly dreaming or skill learning. Dreaming merges every conversation's transcripts into one memory and can promote facts to the shared KB, and a skill learned from one conversation is offered to all of them, so neither runs for the agent. The dashboard also refuses to apply dreaming proposals left from before the mode was turned on (409).
  • Operator-only persona. No conversation may change AGENTS.md, SOUL.md or IDENTITY.md. The agent is told to decline name or persona change requests, and orchestration workers receive no persona-edit authorization. In an app agent's sandbox these files are mounted read-only.
  • Replies only to the originating chat. Every channel's reply, react, edit and thread tool refuses any chat but the one the session serves (GATEWAY_ORIGIN_CHAT_ID), and refuses everything when that is unknown. On Discord a thread counts as its own chat: the session may also use a thread it opened itself with discord_create_thread, but no other thread, even one under its channel. telegram_download_attachment accepts only a file_id that the receiver delivered in this chat (it keeps the newest 200 per chat in .telegram-state/attachments/), and discord_download_attachment accepts only Discord CDN URLs. Both save into the session's media directory.
  • Files only from the session's own media directory. Files a tool sends, shares (share_file, line_image, image and video references) or attaches must resolve inside the session's media directory, and the gateway applies the same rule to /v1/shares, /v1/image-artifacts and API attachments. A share is revoked only by its own session.
  • Default-deny MCP tools. Only the originating channel's module and the image, video, share-file and narrate modules load (narrate speaks only through the task bridge, into the task's own conversation); every other module, including browser and any module added later, is off. The operator's own Claude Code MCP servers and the agent's connectors are not injected. Orchestration browser and computer tasks are off, because one browser profile and one computer serve every conversation.
  • No broad API key, token or gateway address in an app sandbox. A sandbox gets no gateway MCP config, no channel bot token and no GATEWAY_API_URL. A host agent's MCP server gets only a non-admin API key whose agents list names the agent, never a wildcard or admin key; WhatsApp and WeChat replies need such a key with write: true.
  • Cross-chat API needs an admin key. For an isolated agent, /v1/agents/:id/chats… (chat list, sessions, messages, search, active days and session inject) and GET /v1/agents/:id/media/* for a channel chat's media refuse a non-admin key with 403. An API session's own media (media/api-<session>/…, which reply attachment URLs point at) stays readable with the agent's key, because an API client addresses its sessions itself.
  • No cron jobs. Creating a cron job for an isolated agent is refused, and the agent's existing jobs, including ones created before the mode was turned on, do not run. They stay stored unchanged, so a one-shot job is not used up. type: command jobs, which run on the host, are refused for every app agent too.
  • No --append-system-prompt-file in claude.extraFlags. Claude Code keeps only the last such flag, so it would replace the conversation's memory. The config loader refuses such an agent; use --append-system-prompt or move the text into AGENTS.md.

A running agent keeps the mode it started with. When the setting changes, through a config edit or an app update or reconfigure that changes isolate_memory, the gateway restarts that agent once none of its sessions is busy; no gateway restart is needed. Memory is keyed by the conversation (channel and chat). An app that groups several chats into one account, such as one family with several linked parents, still gets one memory per chat.

The app's own API is outside the gateway's reach: if the app hands its agent one token that serves every tenant, that token can still reach any tenant, so the app must scope it.

Implementation: workspace loader, session spawn, conversation sandbox, MCP isolation.

Turning the memory archive off ​

The searchable memory archive (kb.sqlite) is one index over the agent's MEMORY.md, USER.md and every memory/*.md file, with no conversation boundary. An app that keeps its own per-tenant memory files, such as memory/family-<id>.md for a family with several linked chats, cannot use isolated memory mode, because that mode keys memory by chat. Without another switch, a memory_search made while serving one family can return another family's notes.

disableArchive turns the archive off without changing how memory is injected:

  • Host agents: agents[].disableArchive in the gateway config, true or false. The config loader skips an agent with any other value.
  • App agents: services.agent.disable_archive in app.yaml. At install, update, reconfigure and every gateway start, the gateway writes it into the agent's disableArchive, with the same lifecycle as isolate_memory.

When it is on:

  • The archive is not reindexed when a session spawns, so no kb.sqlite is built.
  • The memory MCP tools (memory_search, memory_get and the memory_shared_* tools) are not offered.
  • The composed CLAUDE.md leaves out the memory retrieval note, core-shrink and the write-routing addendum, because each one points at memory_search.
  • Nightly dreaming moves nothing out of MEMORY.md: the compaction of finished (MERGED/CLOSED) entries into memory/archive/, the archive staleness pass and promotion to the shared knowledge base are off.

It does not turn on isolated memory mode: USER.md and MEMORY.md are still injected for the whole agent, and the other MCP tools stay available. The two switches can be combined. A running agent keeps the setting it started with; when it changes, the gateway restarts the agent once none of its sessions is busy. An existing kb.sqlite from before the switch is left on disk but is no longer read or updated.

Nightly session compaction ​

Memory dreaming consolidates workspace memory files. Session compaction separately reduces a resumed Claude Code conversation using its native /compact command. It does not delete stored chat history, rebuild the conversation from recent messages, or ask a question when a user returns.

Enable it explicitly in gateway.sessionCompaction; individual agents[].sessionCompaction fields override the gateway values:

json
{
  "gateway": {
    "sessionCompaction": {
      "enabled": true,
      "thresholdPercent": 50,
      "quietMinutes": 60,
      "maxSessionsPerRun": 5
    },
    "dreaming": {
      "dreamHour": 3,
      "dreamMinute": 0,
      "dreamTimezone": "Asia/Bangkok",
      "staggerWindowMinutes": 30
    }
  }
}

The compaction defaults are enabled: false, thresholdPercent: 50, quietMinutes: 60, and maxSessionsPerRun: 5. The threshold is clamped to 1–99%, quiet time to 1–10,080 minutes, and the run limit to 1–100 sessions. The schedule inherits the agent's effective dreaming hour, minute, timezone and deterministic staggering. Memory dreaming may be disabled while session compaction remains enabled. Orchestration must be enabled. Restart the gateway after editing these configuration fields to apply the schedule consistently.

Each nightly sweep examines up to 1,000 recently mapped native sessions. A session qualifies only when its latest measured context exceeds the configured percentage of the selected model's known context window. Sessions with active tasks, a running agent, pending input, recent conversation activity, missing measurements, an unknown model window or an unavailable native transcript are skipped. Eligibility is checked again immediately before compaction; normal incoming messages retain their usual admission path.

Only one nightly compaction runs at a time across the gateway. Sessions sharing a native transcript share activity checks and compaction fences. The per-agent limit bounds native compaction attempts, and a shutdown stops new attempts. A successful compaction is not repeated against the same old measurement; another conversation turn must first produce fresh usage. A previously recorded manual compaction or /clear also prevents using the old measurement for a nightly attempt. Context changes during model lookup are checked again before maintenance. Runs interrupted by process termination remain visible in the audit report.

Open Nightly dreaming in the admin dashboard to see both memory dreaming and session compaction, newest first, with date, agent, kind and status filters. Select a run to see proposals or per-session outcomes, skip reasons and safe failure codes. The view refreshes while visible. Audits persist in the agent's orchestration database; memory dreaming retains its workspace audit files.

beforeTokens initially uses the latest recorded context measurement. When the confirmed native compact boundary supplies preTokens and postTokens, the audit stores those values instead. Host sessions can also read this metadata from at most the last 1 MiB of their existing CLI transcript, matched to that compaction's time interval. This is a passive file read, never an additional model request. Missing values remain —; no follow-up prompt or repeated compaction is sent to measure savings.

The dashboard defaults to All activities, with every status selected except Skipped. One table layout is used for both activities. Session compaction reports omit scheduled agents, empty sweeps and sessions that were not compacted. It shows before/after tokens, both as percentages of the recorded context window, plus reduction in tokens and percent of the before value. Unknown after values have no reduction estimate. Memory dreaming reports remain available under the Activity filter; raw audit outcomes remain available through the API without completedOnly=true.

Compaction itself calls the model and can consume provider quota; the report does not. The threshold is a trigger, not a promised resulting context size.

Dashboard dates, date-range boundaries, and Overview hourly charts use gateway.timezone (UTC by default). 24h means today since local midnight, not a rolling 24-hour period. Compaction summaries show before → after tokens, context-window percentages, and the reduction relative to the before value; timestamps also show elapsed age.

Overview has four independently filtered charts: Token activity, Tokens by agent, Tokens by model, and Context reuse. Token activity and Context reuse use hourly points for today and daily points for longer ranges. Agent bars show combined Agent + Worker volume in each agent’s identity color. Context reuse compares today’s measured reuse with yesterday’s full day, independently of the selected chart range. All charts use recorded usage in gateway.timezone, rather than billing cost. See Overview charts for measurement details. Needs your attention appears only when tasks await input or reconciliation; inspect details in the dashboard and respond in the original agent conversation. Theme controls and full-height detail drawers are shared across dashboard pages and the standalone token report.

Claude Code inside.