45 KiB
Persistent Mind session: our Body today, and prior art
Ruling: eureka-body:ruling:mind-self-is-a-persistent-session (Operator, 2026-10-31), joined by eureka-body:ruling:persona-is-append-only-session (Operator, 2026-10-31).
mind-self-is-a-persistent-session: "My hope was that the Self would behave much like my session with you does. You communicate with me about the stuff I asked you to work on, I tell you what's needed, your context persists until clear or compaction, a decision which in Eureka should be based on data"
persona-is-append-only-session: "The personas should also be such a persistent append only session that occasionally refreshes, otherwise we're wasting cache"
Facts only, with evidence pointers. No conclusion or recommendation is drawn. Huginn read at main c7cd9ee (clone, autocrlf false). Web sources accessed 2026-10-31. Marks: [P] primary (vendor docs or repo source), [S] secondary, [N] not verified in this pass. File:line refs are into Huginn main unless a repo is named.
Earlier research this pass builds on and does not repeat: gamecult-ops/docs/research/persistent-self-context-lifecycle.md (5c7dfc8; cited below as LIFECYCLE with its section numbers) and docs/research/2026-10-31-persona-memory-and-voice-prior-art.md (Huginn branch eureka-body/persona-memory-voice-note, bfdf4db).
Part A. Our Body today
A1. Persona turn (crates/mind-body/src/persona.rs)
How a turn is built and called
- One
mind-persona@<instance>start takes up toMAX_TURNS_PER_START = 4turns (persona.rs:61, loop at:577-590). It adopts the Persona run the heartbeat opened; no run held, no turns (:576, "only the heartbeat opens Persona turns", header:6-8). - Each turn makes one tool-less connector call whose output is constrained to the
PersonaReplyschema (:12-17,:282-296TurnRequest,:239-280reply_schema). Reply fields:say[](room enum, text),start_self_run(null or brief/repo/campaign, brief <= 1000 chars). The reply is decoded, never scanned (:749-760). TurnRequestcarries: caller,request_id(<run label>-t<turn>,:659),max_cost_micro_usd(what remains of the run cap),instructions,input,output_schema_json(:282-296). It has no session handle or reasoning-state field. The transport buildsProviderRequestwithinput = [InputItem::UserText { text }]andoutput_schema_json = Some(..)(connector.rs:72-74): one user text item per call.- Gates per turn: brake/envelope read before the call and again before each send (
:615-619,:684-688); spend readout must have a row for her caller or the run holds (:634-645); window turn orused >= budgetends the start (:647-650). - After the call: each
sayis sent with request id<batch>-<index>(idempotent repeat,:682-698);start_self_runis admitted as a follow-up and opens a Self run (:703-715,:763-797); trace is recorded; deliveries are acked through the batch max seq (:717-721).
Tools. None in the turn (tool-less). The claude-connector argv always passes --tools "", --strict-mcp-config, --setting-sources "", --disable-slash-commands, --permission-prompts none, --system-prompt-file (ModelConnector crates/claude-connector/src/launch.rs:112-137). Rulings that change this are in force but not implemented in this code: persona-turns-read-only-tools (Superseded in part by persona-git-personal-sandbox: turns run git inside her bubblewrap sandbox, writes beyond it stay in Self runs), persona-unit-token-reach-bounded (mind-persona@<i> may borrow the token under bounds: whole turn in bwrap, tools confined, Persona-sized reservation, lease bound to the turn's main process before any Persona lease row is configured), cli-over-mcp-agent-surface.
State that carries across turns. Within one start, only locals: after_seq, answered, failure, self_run, start (:567-571). Across starts: nothing in the process. What persists is in the mind and Bifrost: runs and follow-ups (mind), the pending queue and ack position (Bifrost), the Pulse/trace records. The conversation itself is never carried: every call is a fresh claude -p with no handle (see A5). The connector's own comment: a session is "a cache of the caller's transcript; the transcript is the truth" (ModelConnector .../handle.rs:1-4).
How a batch of deliveries is chosen. organs.chat.pending(after_seq) then next_batch (:726-736): sort by seq; take the oldest prefix that shares a conversation, either all public-room deliveries together or one DM room alone; truncate at the first delivery that switches is_direct or (for DMs) room. Ack through the batch's max seq. A wake with no deliveries is allowed for turn 0 (:625-628).
Per-turn content, in the order it is assembled (this answers the cache-layout question; placement is exact):
| # | Content | Where it goes | Source / line | Changes when |
|---|---|---|---|---|
| 1 | Doctrine files text (MIND_PERSONA_DOCTRINE, <= 96 KiB) |
instructions (system prompt file), first |
read_doctrine :391-405, re-read every call; :655 format!("{doctrine}{persona}\n\n{campaigns}") |
the files are edited |
| 2 | Persona state JSON, pretty-printed, prefixed "You are the persona whose state follows" | instructions, after doctrine |
HuginnPersona::instructions :379-387, render_instructions :431-434 |
persona_put |
| 3 | Campaigns in force (slug: title list) | instructions, last |
campaigns_in_force :407-418, render_campaigns :420-429, :654-655 |
a campaign opens/closes |
| 4 | "Rooms you may speak in: ..." | input (the one user message), line 1 |
render_input :948 |
config |
| 5 | Static explanatory paragraphs on message framing/roles | input |
:949-952 |
code |
| 6 | Burn line (spend readout of her caller row, heat, live Mind-run count) | input |
burn_line :873, built :652 |
every turn (ledger moves) |
| 7 | "Your latest runs:" 5 lines (RECENT_RUNS = 5, label, turn kind, how it ended) |
input |
recent_runs :835-854, :72 |
any run starts or ends |
| 8 | "No new messages. This is a scheduled wake..." | input, only when no deliveries |
:958-960 |
wake turns only |
| 9 | Per delivery: its context lines (as earlier), then the message line (room, sender, role, name, to_you, to, text) |
input, last |
:962-971, message_line :936, addressees :924 |
every turn |
So the system prompt (rows 1-3) is stable across turns while those files/state/campaigns are unchanged; the user message begins with stable lines (4-5) followed immediately by per-turn lines (6, 7) and then the deliveries (9). Nothing is appended to a prior turn's content because each turn is a separate call with no history. The reply schema's room enum (reply_rooms :738-747) and campaign enum (:239-280) are part of --json-schema (not the prompt) and vary with the batch and campaigns in force.
The Bifrost delivery's context is up to 20 lines / 16 KiB read back from Matrix per delivery (A4), so consecutive turns in a busy room re-send overlapping room history inside input.
A2. Self runs (crates/mind-body/src/self_run.rs)
- Process. One
claudechild per run, spawned byClaudeSpawnerin themind-self@<instance>unit, with a freshCLAUDE_CONFIG_DIRcopy per child seeded fromMIND_SELF_CONFIG_SEED(:104-105,:628-700). Credential = lease descriptor (CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR), not env (:80-82, header:19-27). - Args (
claude_args,:165-186):-p --input-format stream-json --output-format stream-json --verbose --session-id <uuid> --max-budget-usd <run budget> --max-turns 500 --permission-mode bypassPermissions --permission-prompts none --setting-sources user. The doc comment (:155-164): "no resume, continue or fork, no model and no settings of its own belong to this actuator". - session_id (
:151-153):Uuid::new_v5(&SESSION_NAMESPACE, run.id.0.as_bytes()), namespaceUuid::NAMESPACE_OID(:96); "derived, never stored". The run id is new per run, so each run names a new session. - Start message (
:103-108): "Use the eureka skill. You are Self for the instance<slug>; your run is<run id>. Admit as faculty SelfFaculty, agent mind-self-run." plustask_section(:117-...): per claim, the claimed follow-up'sitemtext verbatim, or a pointer "read it withhuginn view" for a cut spec (c7cd9eemessage: "the start message carries every claim's task"). - Input shape. The start is written as one stream-json user line
{"type":"user","message":{"role":"user","content":text}}(drive:575-576); the same line shape carries the flush (FLUSH_MESSAGE:148). - MAX_TURNS
= 500(:76), "a backstopclaudeenforces; the ceiling is reached first". - Context accounting.
read_usage(:204-218) takes each main-thread assistant message'sinput_tokens + cache_read_input_tokens + cache_creation_input_tokensas the request's prefix; subagent requests are ignored.CEILING_TOKENS = 300_000asks for the flush (:64);FLUSH_HEADROOM_TOKENS = 60_000,FLUSH_GRACE_S = 900,ESCALATE_AFTER_S = 120,EXIT_DRAIN_S = 2(:67-73). All timing decisions are one pure function,decide(self_run/decide.rs; rulingself-run-decider). - How a run ends.
decidecloses stdin at the firstresultunless a flush is outstanding (decide.rs:123WriteFlush,:133CloseStdin); the child exits; an overrun flush is interrupted, then SIGTERM, SIGKILL (header:1-14).carryalways resolves her run Recorded exactly once (:489-503,close_if_open:451-464) with the last result as the reason, or "claude gave no result".settlereports per-model usage to the lease (:526-536). - Result excerpt. Reason text is cut to
REASON_EXCERPT_LIMIT = 800bytes (:542); the journal getsRESULT_TEXT_LIMIT = 4096(:540), viadescribe_result(:546-563). The stream is otherwise discarded; no transcript is kept (the runtime directory and fresh config dir go with the unit,:672-680). - Policy in force that this implements:
self-lifecycle-flush-rehydrate(2026-10-20): no summarizing compaction; a run flushes to the mind and ends; "at wake: a cold cache means fresh rehydration; a warm cache with the previous run's thread open may resume it"; ski-rental rental rule; "each run records its prefix size, rehydration size and cache state so the constants are measured". Of these, the ceiling and flush are in code; resume, the rental rule and the recorded sizes are not inself_run.rs(the only occurrences of "resume" are the doc comment at:155-164). - Probe evidence for the wire:
crates/mind-body/src/self_run/claude-2.1.268-stream-json-probe.jsonlshows aresultevent withusage,modelUsage,num_turns,session_id,terminal_reason,queued_turn_count,total_cost_usd; and...-assistant-usage-records.jsonlrecordscache_creation_input_tokens,cache_read_input_tokens,ephemeral_5m_input_tokens,ephemeral_1h_input_tokens.
A3. launch.rs and heartbeat.rs: what decides when a Mind acts
- launch.rs is "the one way her organs open a run and start its unit" (
launch.rs:1-).open_and_launch(:321): reads in-force runs of the request's turn; a live one answers Busy, a dead one is closed Recorded; checks claims, repetition breaker, admits the run, then starts the unit. Units:mind-persona@<instance>.serviceandmind-self@<instance>.service(:113-125). "A Persona turn's run opens with no claims."START_GRACE_S = 120(:35). At most one live Self run per instance (rulingone-live-self-run). Nothing in launch.rs knows of sessions. - heartbeat.rs on main (financial): each Persona holds initiative in dollars of the swarm's run cap T; it regains at
rate / sum(shares) * share * urgencyper hour, only when idle, capped at T; a turn burns it toT - spend; a full-initiative idle Persona may take a turn; at mostkrun at once, earliest-to-fill first; a classifier model call rates urgency (heartbeat.rs:1-22,poll:908-...,CLASSIFIER_MAX_COST_MICRO_USD = 20_000:62). Operator tag/name fills initiative outright. The heartbeat never acks; the Persona turn drains. - Branch
eureka-body/heartbeat-joint-lever(merge-base21fb9b2, 19 commits ahead of main, 60 files, +2217/-4142): replaces the dollar initiative with a tempo. Header (heartbeat.rs:1-22on the branch): the Minds share one heartbeat and jointly hold the lever; tempo istempo::tallyof every Mind's newest vote (aPulsein her own mind;tempo.rs:23, lower median, default beattempo.rs:13); when a beat falls exactly one Persona takes a turn, chosen bytempo::pickover idle, released Minds by how long each has waited, bent by self-declared urgency, a member addressing her; score(waited / beat) x 10^(LOG_BOUND_ORDERS x urgency / 1000)(tempo.rs:65-68). Two launches happen off the beat: the operator tagging/naming her (once per message) and her own Self run having ended since her last turn. "Nothing is metered here: no meter and no model call." New kindPulse(schemas/cultnet/epiphany.pipeline.pulse.v2.schema.json). Rulings:shared-heartbeat-joint-lever,self-declared-urgency-etiquette,operator-mention-fills-initiative. Not merged. - What starts a Self run today: only a Persona turn's
start_self_runreply (persona.rs:703-715), claimed as a follow-up (rulingself-run-brief-claimed-follow-up); the heartbeat branch also launches a Persona turn when her Self run ended. There is no path by which a member's message reaches a Self run directly (rulingmembers-direct-self-runsexists in the mind; not read in this pass).
A4. Bifrost Matrix mouth (Bifrost main tools/persona-matrix-mouth.mjs, 506 lines)
- Ear. One
/syncloop per Persona; every message in her configured rooms and her DMs becomes a delivery tuple withseq,roomAlias,roomId,eventId,sender,context,mentions(m.mentions.user_ids) and whether the room is a DM (:2-9,:240-243). First sync's history is context, not mail (:213,:52). - Position and ack. The
/syncnext_batchis persisted only when an ack covers it; a restart before the ack redelivers (:7-9,:261-274). Sequence numbers persist across restarts (:257). Unanswered mail is not outrun: atmaxPendingthe sync position stays (:209,:303). - Delivery to the Mind. Reading wakes nothing; the heartbeat (and the Persona turn) polls with a non-consuming
chat_pending_request(:8-9,pendingAfter:277-279,pageOf:356-370, reply capped bylimits.replyBytes); the Persona acks what it read (ack:272-275). - Context. "Matrix is the record": a delivery's
contextis read from the homeserver per delivery (messages before it, room or thread), at mostCONTEXT_LINES = 20lines,CONTEXT_BYTES = 16 KiB,FETCH_LIMIT = 100events per fetch; a failed fetch gives empty context and a log line (:13-17,:35-38,:164-204). - Reply.
send({requestId, persona, roomAlias, text})PUTsm.room.messageusing the request id as the Matrix transaction id, so a repeat is the same event (:281-296). One CultNet RUDP endpoint on loopback serves all Personas (:372-); the Persona's Matrix token is held only by the mouth (rulingpersona-matrix-account-interim).
A5. claude-connector (GameCult/ModelConnector, head c1802d8)
- Two paths. (1) The transport path serves
TurnRequests (Persona turns): the connector spawns oneclaudeperexecute(testsone_spawn_per_execute,backend/tests.rs:1623), argv inlaunch.rs:112-137(-p --output-format stream-json --verbose --input-format stream-json --model ... --system-prompt-file ... --tools "" ...,--json-schemawhen the request has a schema). (2) The lease path lends the subscription token to a Self run's own process (lease.rs). - Resume exists in the transport path.
ClaudeCodeHandle::Ended { session_id, instance, prefix_sha256 }is returned insideReasoningState; the tag is an HMAC (connector handle secret) over caller, instructions, model, session, instance and every transcript item before the handle (handle.rs:12-30,Binding:48-58). The next call is plannedPlan::Resume(argv--resume <session_id> --fork-session,launch.rs:133-138; "a resume always forks",:107-110) orPlan::Seed(fresh session seeded from the caller's transcript) (handle.rs:137-145). - Schema turns hand out no handle. "A schema turn leaves the CLI's StructuredOutput tool exchange in its session, and a later turn without the schema runs with no tools: it must not resume that session" (
backend.rs:269-272). The handle is minted only whenoutput_schema_json.is_none()(:273-292). Persona turns always send a schema (persona.rs:663), so today no Persona turn can produce or consume a handle. Tools in a request fail before spawn (PlanError::Tools,handle.rs:123-132; testtools_or_final_tool_result_fail_before_spawnbackend/tests.rs:1309). - Child environment is exactly
PATH, HOME, CLAUDE_CONFIG_DIR, CLAUDE_CODE_MAX_OUTPUT_TOKENS, CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR(launch.rs:44-53): noCLAUDE_CODE_PROMPT_CACHE_TTLis set, so TTL is the CLI default. - Lease path and a long-lived process.
lease.rs:1-19: the connector is the only token holder and the owner of Self-run admission; a Self run asks over a unix socket (peer uid and systemd unit invocation decide who may ask); the reply is one pipe fd with the token plus plan usage windows. "The lease lasts while the holder keeps the socket open: it ends when the holder closes it, when its unit invocation ends, or when its expiry passes." "The connector spawns nothing here: the Self run'sclaudestays in the Self run's own unit." The lease request carriesexpires_at_unix_ms(self_run.rs:507); the connector bounds it with--lease-max-ms(daemon.rs:100-102), which must be >= the unit'sRuntimeMaxSec(self_run.rs:510-512,MIND_SELF_RUN_CAP_S). Billing: rulingself-run-lease-billing-and-binding(reserved, billed at true per-model cost on settle, bound to one unit invocation). So a lease supports a long-lived process bounded by the unit invocation andlease_max_ms; the lease protocol itself holds no session notion. Whether aclaudecan be resumed under a new lease: the CLI side is in Part B1; the connector stores no Self session.
A6. Rules on the table that assume the current split
eureka-body:cut_spec:cut-persona-memory-write.r1(InForce, brancheureka-body/persona-memory-write, basec8749c5, depends onheartbeat-joint-lever). Adds memory kindepiphany.pipeline.memory.v2in her own mind;PersonaReplygains a requiredremember[](<= 8; text <= 1000 B,aboutfrom batch sender ids + operator ids, salience); organ deriveslearned_in,direct,sourcesfrom the batch;admit_turn_documentsreplacesadmit_pulse(memories + Pulse in one admission, after the says); newrecall.rsselects what memory enters each turn (relevant viaquery_nearover memory/follow_up/ruling top 8;aboutthe batch's senders, limit 6; newest 4; cap 16 items / 16 KiB) andrender_inputgains a "What you remember" section after runs and before messages. Stated premises: "A Persona turn is a tool-less call constrained to one output schema (TurnRequest), so it cannot run a CLI; the reply is the only act a turn has";forbidden_writersincludes "a recall cache across turns";derived_state: "the recall section is derived per turn and never stored"; negative check:chat.pending|ContextLinezero hits in recall.rs. Operator scenario: DM writes a memory, restart persona unit (context gone), ask in public room, she knows it from memory.- Persona voice plan. Spec says the voice cut "rewrites
render_instructions" (file_changes entry atpersona.rs:436); the design is in thepersona-memory-and-voicemap, not yet written (rulingpersona-voice-outside-minds: "how each turn's context carries the Persona strongly enough to hold it is the persona-memory-and-voice map's to design"). Notebfdf4dbresearch note is its prior art. - Rulings (text read this pass):
persona-memory-written-not-transcript(2026-10-31): each turn writes what is relevant to her persistent memory; "cross-room or older transcript in the prompt is the transcript standing in for memory"; room context in a turn is conversation context only (rulingchat-context-replies-see-the-full-exchangestill holds, per its text).persona-voice-outside-minds(2026-10-31): Persona voice everywhere but#minds; coordination chatter stays in#minds.room-registers(2026-10-31):#mindsclipped coordination only;#swarmMinds and humans, Persona voice;#burnopen to all, Persona voice; coordination aimed at#swarmby persona states moves to#minds.persona-unit-token-reach-bounded(2026-10-31): see A1.persona-turns-read-only-tools(2026-10-31, Superseded in part bypersona-git-personal-sandbox): the turn "becomes a bounded tool-using loop with an allowlist and per-turn turn and spend caps"; its tools run as her own user.persona-git-personal-sandbox(2026-10-31): each Mind has a persistent personal sandbox directory; whole turn under bubblewrap (bwrap-whole-turn), credentials and state hidden, rest read-only, network kept for huginn and API; pushing and admitting stay in Self runs.persona-reply-typed-by-schema,persona-turn-drains-deliveries,self-run-credential-lease,self-runs-through-connector: titles only read this pass.
- Both new rulings (
mind-self-is-a-persistent-session,persona-is-append-only-session) say the refresh/clear decision "follows data, not a fixed rule" and that Persona turns today "rebuild the whole context per call".
Part B. Prior art
B1. Claude Code and the CLI (current docs, accessed 2026-10-31)
Session flags [P] https://code.claude.com/docs/en/cli-reference:
--session-id <uuid>: "Use a specific session ID for the conversation (must be a valid UUID)".--resume/-r <id|name|transcript .jsonl path>;--continue/-cmost recent in cwd (skips-p/SDK sessions unlessclaude -p --continue);--fork-session: "When resuming, create a new session ID instead of reusing the original".--no-session-persistence(print only);--name/-n.-p --input-format stream-json --output-format stream-json;--replay-user-messagesre-emits stdin user messages for acknowledgment;--include-partial-messages.--max-turns: "With--input-format stream-json, a message still queued when the limit ends a turn stays queued and starts a new turn with its own limit".--max-budget-usd(print only, client-side estimate; spend can pass the cap; "When you return to a conversation with--continueor--resume, totals [restored from earlier runs]", so a resumed cap counts the whole conversation) [P]cli-referencerow,agent-sdk/cost-tracking.--system-prompt,--system-prompt-filereplace the default prompt;--append-system-prompt[-file]append;--system-prompt-snapshot offrebuilds the prompt every request instead of reusing the one recorded on the conversation's first request;--exclude-dynamic-system-prompt-sectionsmoves per-user context (e.g. auto-memory path) from the system prompt into the first user message "for prompt-cache reuse across users and machines" (ignored with--system-prompt[-file]);--autocompact <auto|tokens>(v2.1.221+).
Long-lived print-mode process [P] https://code.claude.com/docs/en/agent-sdk/streaming-vs-single-mode: streaming input mode "allows the agent to operate as a long lived process that takes in user input, handles interruptions"; "Queued messages: send multiple messages that process sequentially, with ability to interrupt"; "Context persistence: maintain conversation context across multiple turns naturally". The CLI emits one result event per turn; the probe in our repo shows queued_turn_count on it (self_run/claude-2.1.268-stream-json-probe.jsonl). system/init carries a capabilities array (e.g. interrupt_receipt_v1, interrupt_cancel_queued_v1) [P] https://code.claude.com/docs/en/headless. SIGTERM mid-turn leaves the turn unfinished with no result; SIGINT or SDK interrupt() ends it (headless, "Stop a run with SIGTERM"). Our actuator already runs claude as a stream-json process and closes stdin at the first result (A2); the CLI itself does not require that.
Resume [P] https://code.claude.com/docs/en/sessions, .../prompt-caching: sessions are JSONL under ~/.claude/projects/<cwd-encoded>/<id>.jsonl (override CLAUDE_CONFIG_DIR, CLAUDE_CODE_PROJECT_DIR_NAME v2.1.234+), 30-day retention (cleanupPeriodDays); claude -p --resume <id> "prompt" appends to the same session id; --resume searches current project then every other project on the machine (v2.1.223+); resume restores history, model, permission mode (not under -p, which starts in the mode a fresh -p would; bypassPermissions is never restored), not --mcp-config/--settings/--plugin-dir/--add-dir. Resume "sends the whole conversation again, and the request reads from the cache whatever part of its prefix is unchanged and still within the cache lifetime"; by default a resumed conversation keeps the system prompt it started with. "If you resume the same session in two terminals without forking, messages from both interleave into one transcript." A running background session resumed by --resume is attached instead of loaded (v2.1.285+); with --max-turns/--max-budget-usd/output flags the command refuses (exit 1). CLAUDE_CODE_TRANSCRIPT_LOCAL_GC limits a -p session file's growth; CLAUDE_CODE_RESUME_INTERRUPTED_TURN=1 continues an interrupted turn on resume.
Compaction and clear [P] how-claude-code-works, prompt-caching, context-window:
- Auto-compaction: "Claude Code manages context automatically as you approach the limit. It compacts the conversation into a summary." Default window = the model's limit except Sonnet 4.6/Opus 4.6 without extended context at 200K; 1M-native models (Fable, Sonnet 5+, Haiku 5.5, Opus 4.7+) about 967K; settable 100K to 1M via
/autocompact,--autocompact,autoCompactWindow,CLAUDE_CODE_AUTO_COMPACT_WINDOW(LIFECYCLE 2b, line 113). If a single large output refills context after each summary, it stops after a few attempts with a "thrashing" error. /compact [focus]and "Compact Instructions" in CLAUDE.md steer the summary; compaction "by design" invalidates the conversation layer; system prompt layer can stay cached; the summarization request reuses the warm prefix plus an instruction ("costs a fraction"); after a gap beyond TTL it reprocesses uncached ("/compactcosts the most when you resume an old session"); the post-compaction turn writes cache for only the summary.- Idle compaction (v2.1.290+): on the one-hour TTL, "Claude Code can compact it shortly before the cache expires";
idleCompaction: falseturns it off. /clearstarts an empty context (operator-only command; previous session kept and resumable). In-pmode,/model,/effort,/config key=value,/output-style,/reload-pluginswork when typed as the prompt; terminal-only commands do not (headless). Whether/compactand/clearare accepted as a stream-json user message in-pis [N] (not stated on the fetched pages).- PreCompact hook: [N], not fetched this pass (
https://code.claude.com/docs/en/hooks). ASessionEndhook exists (headlessSIGTERM section).
Usage and context fields in the stream [P] prompt-caching, headless, local probe: every assistant message's usage has input_tokens, cache_creation_input_tokens, cache_read_input_tokens (our read_usage sums them into the prefix, A2); result has usage, modelUsage (cumulative per model), total_cost_usd, num_turns, session_id, terminal_reason, queued_turn_count; usage.cache_creation.ephemeral_1h_input_tokens / ephemeral_5m_input_tokens report TTL of writes; /usage shows Prompt cache (main) (hit ratio, misses, warm/cold, likely cause of last miss; v2.1.251+/2.1.260+); the status line exposes a prompt_cache object and current_usage; system/api_retry events exist.
Prompt cache in Claude Code [P] https://code.claude.com/docs/en/prompt-caching:
- Request order is "content that rarely changes first": system prompt (core instructions, tool definitions), then project context (CLAUDE.md, auto memory, unscoped rules), then the conversation. "A change to the system prompt invalidates everything." Plan mode, skills, output-style changes, permission-mode changes, file-change notices and CLAUDE.md edits are appended as conversation messages / ignored until restart, so the prefix stays intact. A file-change notice block is appended mid-conversation and marked for caching.
- Invalidators: model switch (per-model cache), effort level on most models (not Opus 5.5/Sonnet 5.5/Haiku 5.5/Fable 5.1 with key or subscription), fast mode toggle, MCP/tool-definition set change when tools are loaded upfront (deferred tool search keeps the first request's tool list for the whole conversation), compaction, accumulated images (batch removal), Claude Code upgrade.
- TTL: main-conversation bucket, which explicitly includes "non-interactive
-pruns, and Agent SDK turns", gets 1 hour only on a Claude subscription within plan usage; 5 minutes with usage credits, API key or cloud provider. "Everything else" (subagents, workflows, forks, compaction, titles) 5 minutes. Controls:CLAUDE_CODE_PROMPT_CACHE_TTL/promptCacheTtl,CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL,ENABLE_PROMPT_CACHING_1H,FORCE_PROMPT_CACHING_5M(v2.1.242+). Verify withclaude -p ... --output-format jsonandusage.cache_creation. - Cache scope: "effectively scoped to one machine and directory": the system prompt embeds auto-memory paths and the conversation opens with working directory, platform, shell, OS. Parallel sessions in the same directory build matching prefixes and can read each other's cache; sequential sessions share a prefix only if the startup git-status snapshot matches. Fork: "inherits the parent's system prompt, tools, and conversation history exactly, so its first request reads the parent's cache"; a fresh subagent does not.
- How the CLI places breakpoints: the page says only that the cache is organized by that ordering and that it marks the mid-conversation context block "for caching"; the exact breakpoint positions in a resumed or long-lived session are not documented on the fetched pages [N]. The related post
https://claude.com/blog/lessons-from-building-claude-code-prompt-caching-is-everythingis named by the page and was not fetched. - A gateway that strips
cache_controlmakes the whole conversation bill uncached every turn (same page); a 400 namingcache_controlmakes Claude Code move the marker to the last conversation message.
Anthropic API mechanics [P] https://platform.claude.com/docs/en/build-with-claude/prompt-caching (first 100,000 of 165,895 characters read):
- Cache hierarchy
tools->system->messages; a hit requires an exact match of everything up to and including the block markedcache_control; "Any change at one level invalidates that level and all later levels." The page's invalidation table (✘ = invalidated): tool-definition change invalidates tools, system and messages; web search/citations toggle invalidates system and messages;tool_choiceand added/removed images invalidate messages only; thinking configuration andoutput_config.effortare model-specific for tools/system and invalidate messages. (Read from the fetch summary; the ✓/✘ legend was inferred from the "Any change at one level" sentence.) - Breakpoints: automatic caching (one top-level
cache_control, placed on the last cacheable block and moved forward as the conversation grows) or explicit on blocks, up to 4, automatic uses one slot. Lookback: the system checks the breakpoint and walks back up to 20 blocks for entries earlier requests wrote; writes happen only at breakpoints; a breakpoint on content that changes every request never hits. - TTL: 5 minutes default, refreshed free on each hit, measured from the start of the request; optional 1 hour (
"ttl":"1h"); longer-TTL entries must precede shorter ones. Multipliers on base input: 5-minute write 1.25x, 1-hour write 2x, read 0.1x; exceptions: Fable 5.1 and Mythos 5.1 read 0.025x; Opus 5.5 and Sonnet 5.5 read 0.05x. Minimum cacheable prompt 512 tokens for Opus 5.5, Opus 5, Sonnet 5.5, Fable 5, Mythos 5, Haiku 5.5 (1,024 for Sonnet 5; 4,096 for Opus 4.5/4.6/Haiku 4.5); shorter prompts are processed uncached without error. Usage:input_tokens(after last breakpoint),cache_creation_input_tokens,cache_read_input_tokens; total = sum. Not read this pass: pages beyond character 100,000 (cache storage/sharing section, workspace isolation). - LIFECYCLE 2a/2b cite the same numbers and the per-use read statement from the earlier pass (lines 91-113).
B2. Claude Agent SDK session model [P] https://code.claude.com/docs/en/agent-sdk/sessions
- A session is the accumulated history written to disk automatically. Options:
continue(most recent in cwd),resume(id),fork_session/forkSession(new id with copy of history; "the original stays unchanged"; forking branches history, not the filesystem). Session id is on everyresultmessage (session_id); TypeScript also onsystem/init. - Multi-turn in one process: Python
ClaudeSDKClient(eachclient.query()continues the same session; connection held), TypeScript streaming input via an async generator orcontinue: trueper call; streaming input mode is "the preferred way" (.../streaming-vs-single-mode). TypeScript V2createSession()preview removed in 0.3.142. - Persistence:
persistSession: false(TS) orCLAUDE_CODE_SKIP_PROMPT_HISTORY; cross-host resume viasessionStoreadapter mirroring transcripts, or moving the.jsonl, or "don't rely on session resume": pass the results into a fresh session's prompt ("often more robust than shipping transcript files around"). Session files are local to the machine; lookup key derives from cwd. Helpers:listSessions,getSessionMessages,getSessionInfo,renameSession,tagSession. - Limits end with
error_max_turns/error_max_budget_usd; resume with a higher limit; a single-shotquery()raises after yielding the error result.
B3. Letta / MemGPT
- MemGPT paper [P]
https://arxiv.org/abs/2310.08560(via LIFECYCLE 3a, line 159): "virtual context management", tiered memory like an OS hierarchy, interrupts for control flow. The specific memory-pressure warning and flush percentages are [N] here (not re-fetched; the Letta pagehttps://docs.letta.com/guides/agents/architectures/memgptreturned only navigation). - Letta [P]
https://docs.letta.com/concepts/conversations(fetched): one agent has many conversations; all share the agent's identity and long-term memory (MemFS); each conversation holds its own message history for its thread;/forkbranches a conversation; "As it approaches the model's context limit, compaction happens automatically": older messages are summarized and recent ones kept, the full message history remains and the agent can use conversation search to recover earlier context; the summary may omit exact wording or provenance. The page says nothing on concurrent or serialized message handling per agent. - Letta memory page [P] (via LIFECYCLE 3a): memory is MemFS (git-backed), background "dreaming" subagents run after N completed steps or when the window is compacted; heartbeats removed in
letta_v1_agent(2025-10-14). Sleep-time compute [P]https://arxiv.org/abs/2504.13171: ~5x less test-time compute, up to +13%/+18% accuracy, 2.5x lower cost per query amortized.
B4. OpenHands, SWE-agent, Devin, others
- OpenHands [P]
https://openhands.dev/blog/openhands-context-condensensation-for-more-efficient-ai-agents(fetched): LLM-based summarizing condenser runs only once context reaches a size; recent exchanges stay intact; older history becomes a summary of goals, progress, remaining work; "because condensation fires only at the threshold, rebuilding the cached prompt cost is spread across multiple turns"; per-turn cost settles at under half of baseline ("up to 2x"); solve rate 54% vs 53% (SWE-bench Verified subset); baseline cost grows quadratically, condensed linearly; the agent sometimes needs an extra turn to condense. Config (LIFECYCLE 3a):max_size/keep_first(docs 120/4, source 240/2, "docs and code disagree"). - JetBrains "The Complexity Trap" [P]
https://arxiv.org/abs/2508.21433: observation masking roughly halves cost and matches or beats LLM summarization on SWE-agent/SWE-bench Verified; hybrid cuts a further 7%/11% (LIFECYCLE 3a). - SWE-agent: only a
cache_controlhistory processor (last_n_messages: 2) seen on the config page (LIFECYCLE 3a, thin). Devin [S]: sessions split into focused sessions; Knowledge persists across sessions;/handoff(LIFECYCLE 3a). Cline [S]: Auto Compact at ~80% in-task plus anew_taskhandoff at a user threshold (example 50%). Aider [P]:--max-chat-history-tokenssoft limit then summarization; prompt order system prompt, read-only files, repo map, editable files "for caching"; optional 5-minute keep-warm pings, default off. - Anthropic engineering [P] (LIFECYCLE 3a): context rot "a performance gradient, not a hard cliff"; compaction for back-and-forth, note-taking for milestones, sub-agents for parallel exploration; harness post: context reset vs compaction chosen per model generation (Sonnet 4.5 needed resets for "context anxiety"; Opus 4.5 largely removed it).
B5. Concurrency: one agent session serving several conversations
- Letta: one agent, many conversations, shared long-term memory, per-conversation history (above); serialization not stated.
- Claude Code in Slack (legacy version, retired for Claude Tag workspaces as of 2026-10-05) [P]
https://code.claude.com/docs/en/slack: an @mention creates a new Claude Code cloud session; context is gathered from the thread (or recent channel messages) at mention time; it works in channels, "does not work in direct messages"; progress is posted to the thread; follow-ups continue in the web session. Claude Tag (the replacement) was not read [N]. - Agent SDK streaming input: queued messages "process sequentially, with ability to interrupt" (one session, one queue);
--max-turnstext says a message still queued when the limit ends a turn stays queued and starts a new turn. Two processes on one session interleave one transcript (sessions); concurrent resume of a running background session attaches rather than forks. - Our Persona already serializes by construction:
next_batchtakes one DM room alone or all public rooms together per turn (persona.rs:726-736), at most one live Persona run per instance (launch.rs Busy), and the heartbeat joint-lever branch fires one Persona per beat. - LIFECYCLE does not cover room/DM multiplexing; the
persona-memory-and-voicenote (bfdf4db) treats memory, not session multiplexing.
B6. Data-driven compaction / refresh triggers
All from LIFECYCLE (accessed 2026-10-20) unless marked; no source found there applying a formula that combines TTL, rewrite price, idle time and summary size to a compact-or-restart decision (LIFECYCLE 2f, line 142; "ski-rental" only generic theory, line 143).
- Token-fill thresholds in tools: Claude Code auto-compact near the window, 100K-1M settable (above); Anthropic context editing
clear_tool_usestrigger 100,000 input tokens (default) withclear_at_least"to make sure clearing is worth breaking the prompt cache"; compaction API trigger 150,000 default, 50,000 minimum (LIFECYCLE 4a); OpenHandsmax_size; Cline ~80% /new_task~50% [S]; Aider soft token limit. - Cache-state triggers: Claude Code idle compaction before a 1 h TTL expires; model-switch confirmation only while the cache is warm;
/usageseparates "expected rebuilds" (compaction, tool-result clearing) from misses (LIFECYCLE 4a, 2b). Eureka's ownself-lifecycle-flush-rehydrateruling: ceiling ~300k (measured), ski-rental rental rule restarts "once the read cost accumulated above the rehydration size since the last rehydration reaches the cost of one rebuild", at most about twice the hindsight optimum; record prefix size, rehydration size, cache state per run. - Cost of misses, measured in our fleet (
tool-calling-cut.md:28-42via LIFECYCLE 1d): subagent calls since 09-29: gaps < 60 s 25,232 calls / 56M write tokens; 60-300 s 2,336 / 5M; 300-3600 s 1,241 calls / 292M write (avg 235k); > 1 h 10 / 2.5M; "79% of all subagent cache writes come from the 4% of calls that follow an idle gap of more than 5 minutes". Append-only cost modelreads ~ N*70k + 0.9k*N^2; "splitting it in two at the midpoint cuts them by about 40%". Per 10 minutes of waiting, foreground polling costs about 2.2 cache reads (about 0.2 context units) against 1.25 for one rewrite (LIFECYCLE 2f line 139). The operator's note ("after the first turn it's all cached tokens") agrees with the pricing page: read 0.05x on Sonnet 5.5 and Opus 5.5, 0.1x others, write 1.25x (5 min) / 2x (1 h). - Task-boundary triggers: Eureka rulings: end at "a task or phase boundary", per-model quality ceiling, or the rental rule (
self-lifecycle-flush-rehydrate); Anthropic: compaction vs note-taking vs sub-agents by task shape; Devin/Cline: handoff to a fresh session at thresholds. No source found detecting task boundaries from the stream itself (LIFECYCLE 4a, lines 202-203). - Quality-degradation evidence: Lost in the Middle (
arXiv 2307.03172), NoLiMa (2502.05167: 11 of 13 models below 50% of baseline at 32K), LLMs Get Lost in Multi-Turn Conversation (2505.06120: 39% average drop, "when LLMs take a wrong turn ... they get lost and do not recover") (LIFECYCLE 3b). Compaction failure modes [S]tianpan.co/blog/2026-04-19-compaction-traps-long-running-agents: erased failed-tool records -> retry loops, dropped completion records -> redone work, "constraint amnesia". - Uncertainty carried: page-read limits: the API caching page was read to character 100,000 of 165,895 this pass; LIFECYCLE read 100,000 of 160,171 (2a).
B7. One session or two per Mind (Persona and Self)
Facts only.
- In our Body today they are two different processes/units with different owners of the credential path: Persona turns through the connector transport (
mind-persona@, connector spawnsclaude, no tools, schema), Self through the lease (mind-self@, the unit's ownclaude) (A1, A5,launch.rs:113-125). Their system prompts differ (Persona: doctrine+persona+campaigns via--system-prompt-file; Self: default prompt,--setting-sources userwith the seeded config and the eureka skill, start message names the skill) (persona.rs:655,self_run.rs:165-186,:103-108). They are different models/settings only as configured: not checked. - In the CLI, a session is one conversation id with one history and one system prompt fixed at its first request (resumed conversations keep the original system prompt,
prompt-caching). Two sessions with different system prompts, tools or directories share no cache (cache scoped to machine + directory + model + speed + prefix,prompt-caching"Cache scope"). Same-directory parallel sessions with matching prefixes can read each other's cache. A fork shares the parent's cache on its first request. A subagent (separate system prompt) starts its own cache; its calls append only a call and result to the parent, and it gets the 5-minute TTL bucket even on subscription. - In the Agent SDK/CLI, one process can serve multiple user messages sequentially in one session (streaming input), and one session can be resumed or forked; there is no documented multi-session multiplexing inside one process on the pages read.
- Cost per fresh session start: the whole prefix (system + project context + first messages) is a cache write at 1.25x (5 min) or 2x (1 h) of base input; later requests read at 0.05x/0.1x; ledger examples: Opus 5.5 base $4/MTok, read $0.20, 5-minute write $5, 1-hour write $8; Sonnet 4.6 base $3, write $3.75/$6, read $0.30 (LIFECYCLE 2a). Unmeasured here: our actual Persona turn cache-read ratio (the connector child sets no TTL variable,
launch.rs:44-53; no stats were pulled this pass). - Letta and Slack examples (B3, B5): one agent identity with several conversations/sessions sharing long-term memory; Slack legacy: one session per request.
Not found or not verified in this pass
- Exact CLI breakpoint positions in resumed or
stream-jsonsessions (B1). - Whether
/compactor/clearcan be sent as a stream-json user message under-p(B1); PreCompact hook contract (B1). - MemGPT's numeric pressure/flush thresholds; Letta's per-agent concurrency semantics (B3).
- Behaviour of
--session-id <existing id>with an existing transcript (the CLI reference states only "must be a valid UUID"). - Our deployed Persona/Self cache-hit statistics; the actual mind-persona unit file and
RuntimeMaxSec; the claude CLI version in production (connector tests pin 2.1.312 atbackend/tests.rs:78; the probe files are 2.1.268). - Text of rulings
persona-reply-typed-by-schema,persona-turn-drains-deliveries,members-direct-self-runs,chat-context-replies-see-the-full-exchange,self-run-credential-lease(titles/ids only). - Claude Tag (Slack replacement) session model.