Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff6a8206d7 | ||
|
|
707a842dfb | ||
|
|
900320b5d3 | ||
|
|
014f446548 | ||
|
|
d77bbdf355 | ||
|
|
867a9f374f | ||
|
|
8dfcbcdf8b |
3 changed files with 466 additions and 0 deletions
153
docs/research/2026-10-31-live-mind-sessions-sequence.md
Normal file
153
docs/research/2026-10-31-live-mind-sessions-sequence.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# Live Mind sessions: architecture and cut sequence (2026-10-31)
|
||||
|
||||
This is Imagination's map for ruling `eureka-body:ruling:live-mind-sessions` (Operator, 2026-10-31, answers `mind-sessions-talk-and-work` with `two-sessions-one-memory`):
|
||||
|
||||
> "Persona Self is supposed to be a persistent session that compacts and hands off as appropriate, same thing with the Persona that drives it. I'm not getting that feeling with the swarm, here. They should be working side-by-side on their projects, exchanging notes. There should always be at least one active session, with new messages batching and dropping into their contexts between tool calls. Why is that not the case?"
|
||||
|
||||
It revises the cut order of `2026-10-31-persistent-mind-session-map.md` (d77bbdf, cited as MAP) so that live sessions come first and memory-write runs beside them, not ahead of them. The facts come from `2026-10-31-persistent-mind-session-prior-art.md` (8dfcbcd, cited as PA). Huginn was read at main c33884f, `gamecult-ops` at 6ba0482, the live units on Yggdrasil read-only (`systemctl cat`, 2026-10-31), and the Claude Code docs `hooks` and `headless` were fetched 2026-10-31. Inferences are marked as such. Open forks are questions in the eureka mind.
|
||||
|
||||
## 0. Why it is not the case today
|
||||
|
||||
These are four facts about c33884f, and each one is a one-shot owner.
|
||||
|
||||
1. **A Self run dies at its first answer.** `self_run/decide.rs:127`, `Event::Result => CloseStdin`. The `claude` child runs one turn, then the actuator closes its stdin and it exits. The CLI does not require this: streaming input is documented as "a long lived process" with queued messages (PA B1).
|
||||
2. **A brief to a working Self is dropped.** `persona.rs:752-786` `start_self_run` admits the brief as a follow-up. `open_and_launch` then answers `Busy { run }` because a Self run is live, and the Persona takes that as success. The live child never hears of the follow-up, and no later run claims it, because only the start message carries claims (`self_run.rs:124` `task_section`).
|
||||
3. **A Persona turn is one schema call per beat.** `persona.rs` makes one tool-less connector call per turn and takes at most `MAX_TURNS_PER_START = 4` turns per unit start. The heartbeat (`heartbeat.rs`) opens one `PersonaTurn` run per beat through `open_and_launch`, and `mind-persona@` has `RuntimeMaxSec=30min`. No conversation survives a turn (PA A1, A5).
|
||||
4. **Nothing keeps a session alive.** The heartbeat's only verb is "start a one-shot unit". When nobody is mentioned and the beat has not fallen, the swarm has zero processes.
|
||||
|
||||
## 1. The architecture
|
||||
|
||||
### 1.1 Who owns what
|
||||
|
||||
| Session | Process and owner | Lifetime | Reads | Acts |
|
||||
|---|---|---|---|---|
|
||||
| **Self session** (one per Mind) | `claude` child of `mind-self-run` in `mind-self@<i>` (user `mind-<i>-self`). The actuator (`carry` + `decide`) owns its lifetime and everything that enters it. | One process per run. A run spans many briefs. The session spans processes by resume (cut 4). | Her mind; briefs and tells in her Persona's words; never raw chat | Full Self toolset: forge push, admission, sandbox `/var/lib/mind-<i>-self` |
|
||||
| **Persona session** (one per conversation: public rooms together, or one DM room; MAP 2.1) | `claude` child of `mind-persona` in `mind-persona@<i>` (user `mind-<i>`), leased like the Self (question `persona-session-owner`). The Persona actuator owns lifetime and input. | Long-lived. It rotates under the unit cap. | Chat as framed data lines, her mind, read tools in bwrap | Only the decoded `PersonaReply` fields, gated by the organ (say, start_self_run, remember, settle, vote) |
|
||||
| **Session keeper** | `mind-heartbeat` (it already exists) | Always | Every mind's runs, pulses and chat pending | Starts and wakes sessions; never speaks, never acks |
|
||||
|
||||
Every session's truth is the mind. A transcript is a cache (`claude-code-handle-reseeds`; MAP 3).
|
||||
|
||||
### 1.2 How a message reaches a live session, and how she replies
|
||||
|
||||
**Chat to Persona.** The Bifrost mouth's delivery stream is unchanged: deliveries with seq, `chat_pending_request`, and ack (PA A4). The Persona actuator holds the live Persona session. Each poll it reads the pending deliveries (`next_batch` still keeps one conversation per batch) and turns new ones into a single framed user message for that conversation's session. The message is written two ways:
|
||||
|
||||
- **Mid-turn**, by a `PostToolBatch` hook (or `PostToolUse` where `PostToolBatch` is absent) whose `additionalContext` "Claude Code wraps ... in a system reminder and inserts ... into the conversation at the point where the hook fired" (docs `hooks`, fetched 2026-10-31). The hook command is the organ's own binary. It only drains a spool directory that the actuator writes; it decides nothing.
|
||||
- **Between turns**, as a stream-json user line on the open stdin.
|
||||
|
||||
One function claims a spool entry by `rename(2)` into `told/`. Both the hook and the actuator call it, so each message enters the session exactly once.
|
||||
|
||||
Whether a plain stdin user line written mid-turn already lands at the next tool boundary in CLI 2.1.312 is not documented on the fetched pages (`streaming-vs-single-mode` says only "Queued messages: send multiple messages that process sequentially"). The hook is the documented path. Cut 1 probes the stdin behaviour first, and if stdin already lands mid-turn the hook goes. That decision follows the data and is not a fork.
|
||||
|
||||
**Her reply.** In a session each turn still ends with one `result` carrying `structured_output` against the session's constant `PersonaReply` schema. The organ decodes it, sends the `say`s through the mouth, admits the memories and settles (memory-write), and acks through the batch's max seq. Acks still mean "answered". A message dropped in mid-turn is answered in that turn's reply, so the ack covers it. A turn whose reply fails acks nothing (unchanged).
|
||||
|
||||
**Persona to Self (a brief or a tell).** `start_self_run` is admitted as a follow-up with `owner: "self-run"` (unchanged). Her Self's inbox is derived from the mind: her in-force follow-ups with owner `self-run` that this session has not been told yet. The Self actuator polls her mind every `INBOX_POLL_S` (30 s; it is a local daemon) and spools new ones. They land between tool calls like chat. A brief to a working Self is therefore told to that Self, not dropped (fact 0.2). The brief text is her Persona's words, which is the laundering point that already exists.
|
||||
|
||||
**Self to Persona.** Each turn of the Self session ends with a `result`. The Self records what it did (memory `did`, `decided`, `learned`, from memory-write) and resolves the follow-up it finished, with its outcome as the rationale. The keeper wakes her Persona when her Self admits a memory or resolves a self-run follow-up (cut 5). Her Persona sees the resolution and the memory through recall and reports to whoever asked.
|
||||
|
||||
### 1.3 The heartbeat becomes the session keeper
|
||||
|
||||
The keeper's state is a set of facts it reads and never stores: per Mind, whether the Persona session is live (unit active) and whether it is mid-turn (a turn trace open), and the same for the Self.
|
||||
|
||||
- **Floor.** While any Mind's brake is released, at least one Persona session in the swarm is live. The keeper starts `mind-persona@<i>` for the Mind `tempo::pick` chooses when none is live. A live, idle session costs no model call: it is a process blocked on stdin.
|
||||
- **Tempo, new meaning.** The beat is how often an idle live session is woken: a scheduled-wake user message is written into the picked session. It no longer means how often a one-shot unit is started. A second lever, `live_sessions`, is the most Persona sessions kept live at once, the lower median of cast votes with abstention as in `tempo-silence-is-abstention`. Its default is every released Mind, because idle sessions cost nothing but RAM and cache expiry. What "active" means is question `active-session-floor`.
|
||||
- **Wakes off the beat** (operator mention, a Mind naming her per `minds-wake-each-other`, her Self reporting) become messages written into her live session, which is started if it is not live, once per message (the `handled_mentions` rule is unchanged).
|
||||
- **Dead.** The keeper never opens a `PersonaTurn` run per beat. A Persona run is one session process, opened when the keeper starts the unit and closed when the process ends.
|
||||
|
||||
### 1.4 Compaction and handoff, triggered by data
|
||||
|
||||
The ruled shape stands: no summarising compaction (`self-lifecycle-flush-rehydrate` clause 1). Memory is the summary. A refresh is end-and-rehydrate from the mind, and because every turn writes memory, no goal is lost (MAP 1, 2.5, 2.6). The CLI's machinery is used only where it fits:
|
||||
|
||||
- **PreCompact hook: a backstop that blocks.** Auto-compaction would summarise ("constraint amnesia", PA B6). The session's `--settings` carries a `PreCompact` hook (matcher `auto`) that exits 2 ("Blocks compaction", docs `hooks`) and spools a ceiling event. The actuator answers with the flush, which is the same flush the ceiling asks for. The organ's ceiling (Self 300k, measured; Persona to be measured) stays below the CLI window (about 967k on 1M models, PA B1), so the hook fires only if the organ's measure is wrong. Whether to let the CLI compact instead is question `session-compaction-mechanism`.
|
||||
- **Triggers, one pure decider per session** (`decide.rs` grows; the Persona gets its own):
|
||||
1. *Context fill*: prefix >= the per-model ceiling. The flush is asked and the process ends after it.
|
||||
2. *Task boundary*: the session is idle, with no in-force self-run follow-up and nothing spooled. The idle deadline is the cache TTL observed on the last request (`cache_creation.ephemeral_1h_input_tokens` > 0 means 3600 s, else 300 s) minus a 60 s margin. Inside that window a new brief reads a warm cache. Past it the session is cold, and ending it costs nothing that a resume would not pay again.
|
||||
3. *Cache economics*: the rental rule and the system-prompt-input change (MAP 2.5, rules 1-3) apply when a session would be resumed (cut 4).
|
||||
4. *Lifetime*: see 1.5.
|
||||
Every decision is written to the journal with its inputs (prefix, TTL, idle seconds), so the constants are measured (clause 4).
|
||||
|
||||
### 1.5 Lease and unit lifetime past 4 h
|
||||
|
||||
On the live units: `mind-self@` has `RuntimeMaxSec=4h` and `MIND_SELF_RUN_CAP_S=14400`. The lease is bound to the unit invocation and capped by the connector's `--lease-max-ms` (PA A5). A lease is never renewed in place, because renewal would make the connector track a session it does not own. **A session outlives its process instead.**
|
||||
|
||||
- Each process ends at an idle boundary once `now >= started + cap - ROTATE_MARGIN_S` (20 min), with a graceful stdin close. If it is still mid-turn at `cap - FLUSH_MARGIN_S` (10 min), the flush is spooled so that it lands at the next tool call, and the existing grace and signal escalation apply.
|
||||
- Cut 4 (`self-session-resume`) keeps the session's transcript under her StateDirectory (`/var/lib/mind-<i>-self/sessions/<session-id>.jsonl`, copied out of the fresh config dir when the process ends). The next process, under a new lease, starts with `--resume <path>` when the decider says warm, and otherwise rehydrates fresh. Resume by path is documented (`headless`, "pass `--resume` the absolute path to a session's `.jsonl`"). The CLI cache is keyed by prefix, not by process, so a rotation inside the TTL reads cache. The transcript is a cache. The seed-fresh config rule of `self-run-persistent-sandbox` holds, because the transcript is the session's own context and never settings.
|
||||
- The Persona works the same way: its unit cap rises from 30 min to the same 4 h shape and its process rotates.
|
||||
- The alternative of raising the caps is question `session-lifetime-over-cap`.
|
||||
|
||||
### 1.6 Minds exchanging notes
|
||||
|
||||
A note from one Mind to another goes Self → her Persona → `#minds` → the other Persona → her Self. `#minds` is the register for coordination (`room-registers`, `persona-voice-outside-minds`). Each Mind keeps one voice owner. Text reaches a Self only in her own Persona's words, and naming a Mind wakes her (`minds-wake-each-other`, which becomes a message written into her live session). Latency is one Persona turn on each side, and both sessions are live, so this takes seconds. The direct alternative is question `mind-notes-route`.
|
||||
|
||||
### 1.7 Plan-usage budget visibility
|
||||
|
||||
Today the lease grant carries the plan windows once, rendered into the start message (`self_run.rs:108`, `render_plan`), and the Persona's burn line comes from the connector's spend readout. A live session needs both to be current:
|
||||
|
||||
- Each batch written into a session (a tell, chat, or a wake) ends with one plan line: windows, used, reset, and "as of" time.
|
||||
- The Persona reads her burn line as today.
|
||||
- The Self needs a fresh readout. The connector's lease socket gains a read-only `plan` request answering the same `PlanQuota` windows to a lease holder. That is cut 3 (`plan-readout-live`) in ModelConnector. Until it lands, the line says "as of grant", which is honest and stale.
|
||||
|
||||
### 1.8 Prompt-injection bounds
|
||||
|
||||
`persona-turns-read-only-tools` says the worst a hostile chat message can make a turn do is read. The two-session split keeps that true. Chat never enters a session that holds push and admit.
|
||||
|
||||
- **The Persona session reads chat** and can act only through decoded reply fields the organ gates. Its tools are read tools and her sandbox git under bwrap (`persona-git-personal-sandbox`). A long session keeps a planted instruction in context for longer (MAP 2.8). The bound is the same, because the reply is still the only act.
|
||||
- **The Self session never reads chat.** Its inbox is follow-ups her Persona admitted (one brief of at most 1000 chars per `start_self_run`) and, later, memories. The Persona is the laundering point, as today. What changes is reach in time: a brief can now land in a session mid-task. The worst a hostile message can do is talk her Persona into writing a hostile brief, which today's one-shot Self also accepts. Nothing new reaches the Self.
|
||||
- **Hook injection is data.** The hook prints only spool entries the actuator wrote from the mind, framed as factual statements naming their source document (the docs warn that "out-of-band system commands" framing trips injection defences). In a subagent (hook input carries an agent id) it is a no-op, so tells reach only the main thread.
|
||||
- **Refresh purges** anything that never reached memory. Memory poisoning is the durable vector, and each memory carries its sources and is withdrawable (MAP 2.8).
|
||||
|
||||
### 1.9 Evidence from the Cinny fork, and what it binds (Self, 2026-10-31 13:4xZ)
|
||||
|
||||
The operator: "Aqua was gonna work on the notification sounds and ambient soundscape for it, Libby was working on the math and shaders for the background, Void was putting it all together. They were coordinating. And of course they just forgot all about it as soon as it rolled off the transcript." Self read the minds of void, libby and aqua. None holds a ruling, and nobody's mind holds the shared plan. In-force `chat-*` follow-ups never close: void 24, libby 23, aqua 22. Libby holds about 10 near-identical "CultMath noise survey" briefs (`chat-s239` to `chat-s318`), one asked per turn because she could not see the earlier answer. Real results exist as follow-ups (`cultmath-ts-noise-absent`, `cinny-estate-navigator-footprint`) and as AquaSynth PR #1.
|
||||
|
||||
This binds two things.
|
||||
|
||||
- **A brief is closed by its result.** An open self-run follow-up must mean open work, or the goals section (goals-in-turn) is noise and cut 1's inbox would re-tell every stale brief. Owner: the Self actuator. At each `result`, every brief told to the session before or during that turn that her Self has not resolved itself is resolved `Recorded` by the actuator, with the result text (cut on a character boundary at 3800 B, as f2e3805) as the rationale. A longer piece of work she takes on is a follow-up of her own, with a different owner. At session start the same rule is applied to the past: a self-run follow-up claimed by a closed run is resolved `Recorded`, citing that run, whose closure rationale already holds the result. Her Persona then sees results where the brief was, not a pile of open asks. In cut `self-session-live` r2.
|
||||
- **Agreements are recorded in two homes, by scope** (ruling `atlas-records-collaborative-work`, Operator, 2026-10-31: "Just like each steward's own Mind should record and remind them of what work is active in their domain, the shared Atlas Mind should record ongoing collaborative work").
|
||||
- *Her own domain* goes in her own mind, through memory-write: what she was asked, what she committed to, what was decided, what she did. That includes her own part of a shared plan (`committed`, `about` the other Minds' ids).
|
||||
- *Work across Minds* goes in Atlas (`huginn@atlas`): the shared plan, who holds which part, the agreements made between Minds, open items and their state. Every participating Mind's turn is reminded of it. Chat is never the record. Each Atlas document stays owned by its publisher, and only the publisher amends it (`atlas-is-shared-mind-open-evidence`).
|
||||
- The two homes do not compete. Her mind holds her commitment. Atlas holds the plan the commitment belongs to, and her memory cites the Atlas document once it exists.
|
||||
- Facts: `huginn@atlas` is not deployed (unit inactive, no `/etc/gamecult/minds/atlas`). Huginn admission is single-writer: a store is canonical to one instance, and `refuse_foreign_instance` keeps instance-rooted kinds to that instance. The earlier ruling says Huginn owes multi-writer admission.
|
||||
- The interim, until Atlas recall lands: memory-write r3 records her own part and her understanding of the others' parts in her own mind, with sources pointing to the same event ids. Once Atlas lands, the others' parts are no longer hers to record. Her memory keeps only her commitment, which cites the Atlas plan.
|
||||
- Atlas multi-writer is one contained cut (operator: "I think the multi writer cut can be kept pretty contained"), mapped in section 2, row A1.
|
||||
- **What Minds build with each other persists** (ruling `minds-rapport-and-brainstorming-persist`: "Their own brainstorming should matter too, the ways they bounce off each other should build a rapport that persists, same as when they interact with humans").
|
||||
- Memory-write r3 adds two sorts. `idea` covers her own ideas and brainstorming, and other Minds' ideas with `about` naming whose idea it is. `rapport` covers how she and another Mind or a person work together, `about` that one.
|
||||
- Rapport's home is her Persona state. `gamecult.persona_state.v0` already has `socialBond` (subject, object target, `relationshipKind` including `collaborator`, trust, tension, `lastEvidence`), so no new kind is needed.
|
||||
- A Persona state change is a system-prompt change, and a system-prompt change invalidates a live session's whole cache (PA B1). So turns write `rapport` memories, and the memory-maintenance cut promotes them into `socialBond`s at a refresh boundary. That is question `rapport-write-path`.
|
||||
- Ideas the Minds develop together go to Atlas (A2).
|
||||
|
||||
**Live gap, found while probing for Atlas (Yggdrasil, read-only, 2026-10-31).**
|
||||
- Every daemon binds `10.77.0.1:1787x`, and nft accepts all `iifname "lo"` traffic. `mind-self@` sets no `IPAddressDeny`.
|
||||
- Each `/etc/gamecult/minds/<m>/huginn.env` is world-readable (0644).
|
||||
- The daemon checks only the instance the batch declares (`daemon.rs:156-162` and `mind.rs:201` `require_instance`, Huginn ffb379d per Self).
|
||||
- Self's own reads of void, libby and aqua went through by declaring `EUREKA_INSTANCE=<m>`.
|
||||
- Inference, from code plus that read path; no admit was sent: any Mind's Self run can admit into another Mind's mind by declaring her name.
|
||||
- This is follow-up `mind-admission-publisher-unproven`. Atlas's publisher proof closes it for every daemon.
|
||||
- Where the operator's rulings about a stewarded campaign live is the existing question `ruling-home-for-stewarded-campaigns`.
|
||||
|
||||
## 2. Cut sequence (revised)
|
||||
|
||||
Live first. Memory-write runs beside cut 1 with no shared file.
|
||||
|
||||
| # | Cut | Repo | Supersedes (one-shot code that must end unable to run) | Runs beside |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **`self-session-live`** r2 (spec admitted): her Self stays alive across briefs; a brief is closed by its result; briefs to a live Self are told between tool calls; it ends at the task boundary, at the cache TTL, near the cap, or at the ceiling; a PreCompact block | Huginn | `decide.rs` `Result => CloseStdin`. A Self process cannot end at its first result, and `Busy` no longer drops a brief. | 2 |
|
||||
| 2 | **`persona-memory-write` r3** (spec admitted): r2 without its two `self_run.rs` changes (they move to cut 5) | Huginn | `admit_pulse` | 1 |
|
||||
| 3 | `plan-readout-live`: lease socket `plan` request; the Self appends a fresh plan line per batch | ModelConnector + Huginn | the start-message-only plan line | 1, 2 |
|
||||
| 4 | `self-session-resume`: transcripts in StateDirectory, `--resume <path>` when warm, rotation under the cap, rental rule; Self capture of memory (from r2) | Huginn | `claude_args` "no resume" and the "fresh session per run" `session_id(run)` | after 1, 2 |
|
||||
| 5 | `self-reports-to-persona`: Self capture paragraph and claim/ask view (moved from r2); keeper wakes her Persona on her Self's new memory or self-run resolution | Huginn | heartbeat's `self_end > last_turn` as the only Self-to-Persona signal | after 2 |
|
||||
| 6 | `persona-session` (merges MAP's `persona-read-tools` r2 and `persona-session`): `mind-persona` holds a leased long-lived claude per conversation, read tools in bwrap, constant schema, chat spooled mid-turn, rotation; unit cap 4 h | Huginn + gamecult-ops | `persona.rs` one connector `TurnRequest` per turn, `MAX_TURNS_PER_START`, the KitConnector transport for Persona turns | after 2 |
|
||||
| 7 | `session-keeper`: the heartbeat keeps the floor, writes wakes into live sessions, `live_sessions` vote | Huginn | `heartbeat.rs` `launch()` opening a `PersonaTurn` run per beat | after 6 |
|
||||
| 8 | Memory maintenance, ruling home, Cut 12 hand-off (MAP 4, items 2, 6, 7) | Huginn | none | any time |
|
||||
| A1 | `atlas-multi-writer`, one contained cut: (1) `huginn@atlas` accepts batches from any roster Mind; (2) each document is rooted at, and records, its publisher, and only the publisher amends or resolves it (`atlas-is-shared-mind-open-evidence`); (3) the publisher is proven, not declared, by a per-Mind key placed by the unit installer the way the forge token is (readable by `mind-<m>` and `mind-<m>-self` only), with an HMAC over the batch that the daemon checks against the declared instance's key. The bound: proof is one key per Mind and one check at admission, the same on every daemon, so it also closes `mind-admission-publisher-unproven`. No roles, no ACL language, no rotation protocol beyond re-install. Deploy is through Idunn and gamecult-ops. | Huginn + gamecult-ops + Idunn | "a store is canonical to one instance" on Atlas; declared-instance admission on every daemon | 1, 2 (rebase onto r3's admission tables) |
|
||||
| A2 | `atlas-collab-recall`: collaboration kinds in Atlas (plan, part held by a Mind, agreement, open item, shared idea); every participating Mind's Persona turn (and Self start) carries a "Shared work you are in" section read from Atlas; a Persona reply can publish or amend her own Atlas items; r3's notes on others' parts are demoted to her own commitment citing the Atlas plan | Huginn | others' parts recorded in her own memory (r3 interim) | after A1, 2 |
|
||||
|
||||
Cut 1 alone gives "a session with messages dropping in" for the Self. Cuts 6 and 7 give "at least one active session swarm-wide". A1 and A2 give "Atlas records ongoing collaborative work".
|
||||
|
||||
## 3. Persistent kinds touched
|
||||
|
||||
| Kind | Identity | Owner and writers | Truth or cache |
|
||||
|---|---|---|---|
|
||||
| follow-up, owner `self-run` (existing) | `<campaign>:follow_up:chat-<batch>` | her Persona's organ admits; her Self resolves | truth: her Self's inbox |
|
||||
| spool entry (new, organ state) | `$RUNTIME_DIRECTORY/inbox/<seq>-<id>.json`, claimed into `inbox/told/` | Self actuator writes; claim by rename from hook or actuator | cache: lost with the unit, rebuilt from the mind |
|
||||
| told set (new, process memory) | follow-up ids told to this process | Self actuator | cache: a new process re-tells open follow-ups, which is correct because they are still open |
|
||||
| session transcript (cut 4) | `/var/lib/mind-<i>-self/sessions/<uuid>.jsonl` | claude writes; the actuator copies it out | cache: never read as memory |
|
||||
114
docs/research/2026-10-31-persistent-mind-session-map.md
Normal file
114
docs/research/2026-10-31-persistent-mind-session-map.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# Persistent Mind session: the map (addendum, 2026-10-31)
|
||||
|
||||
Imagination's map over `2026-10-31-persistent-mind-session-prior-art.md` (8dfcbcd, cited as PA with its part numbers) and `2026-10-31-persona-memory-and-voice-prior-art.md` (bfdf4db, cited as MV). Rulings: `mind-self-is-a-persistent-session`, `persona-is-append-only-session`, `persona-memory-written-not-transcript`, `self-lifecycle-flush-rehydrate`, `persona-turns-read-only-tools` (in part superseded by `persona-git-personal-sandbox`), `campaign-history-moves-to-steward`. Huginn read at main e022e53 plus `eureka-body/persona-goals-in-turn` 90bd201 (treated as landed). This is a design addendum: inferences are marked as inferences; the open forks are questions in the eureka mind, not decisions here.
|
||||
|
||||
## 1. The failure this answers
|
||||
|
||||
The operator asked every Mind to prioritise the Cinny fork. Void later did not know the project or that he stewards it. The ask lived only in room messages (20 context lines per delivery, PA A4); no turn wrote memory (PA A1: the organ admits only follow-ups, run closures and Pulses); the turn never read his stewardships or follow-ups (fixed by goals-in-turn 90bd201); his Self run results were cut to 800 bytes (fixed by 743cbb8). Her rulings about the client sit in Eureka's mind, which Void's turns never read (`campaign-history-bridge-wired` is open).
|
||||
|
||||
A persistent session alone would not have saved him: the ask would have scrolled out at the first refresh. The cure has two halves, in this order:
|
||||
|
||||
1. **Memory is the truth, the session is a cache.** Every Persona turn and every Self run writes what passed through it into her own mind; every refresh rebuilds from the mind. That makes any refresh lossless, which is what lets refresh be a pure cost decision.
|
||||
2. **Sessions keep the cache warm** between refreshes, so turns stop paying for a rebuilt prompt.
|
||||
|
||||
## 2. The design
|
||||
|
||||
### 2.1 Sessions per Mind
|
||||
|
||||
| Session | Holds | Tools | Reads chat | Count |
|
||||
|---|---|---|---|---|
|
||||
| Persona conversation session | one conversation: the public rooms together, or one DM room | read tools and her sandbox git, under bwrap (`persona-git-personal-sandbox`) | yes, as framed data lines | one for public rooms, one per DM room in use |
|
||||
| Self session | her work: claims, briefs, tells, results | full Self toolset, lease, budget | never raw chat; only briefs and asks in her own words, and memories | one (`one-live-self-run`) |
|
||||
|
||||
Two kinds of session, one identity and one memory. Reasons, in the order they weigh:
|
||||
|
||||
- **Injection boundary.** `persona-turns-read-only-tools` (in force in part): "The worst a hostile chat message can make a turn do is read." A session that reads member chat and holds forge push and admission breaks that sentence. The Self is reached only through text she wrote (the brief, the ask she recorded), the laundering point that exists today.
|
||||
- **Cache.** Two prompts with different system prompts and tools share no conversation cache anyway (PA B7, cache scope); merging them would make every chat turn pay for the Self toolset and doctrine, and every Self request carry chat.
|
||||
- **Responsiveness.** A Self task runs for minutes to an hour. A single session would hold her chat until the task's turn ended (stream-json queues messages, PA B1). The operator's own session stays responsive the same way: Self talks, background workers do the work.
|
||||
|
||||
The operator's ruling asks that the Self "communicates with me about the stuff I asked you to work on". In this design the Persona is that voice and the Self is her worker; the person who asked talks to one Mind. Whether the Self should also speak to the asker directly is question `mind-sessions-talk-and-work`.
|
||||
|
||||
Per-conversation Persona sessions are decided, not open: `dm-turns-separate-by-default` says a turn that answers a DM "sees only that DM room's deliveries plus her own state", and `persona-memory-written-not-transcript` says cross-room transcript in a turn is the transcript standing in for memory. One session spanning a DM and a public room would break both. What crosses conversations is memory, marked private where it was learned privately. Letta keeps one agent, many conversations, shared memory (PA B3); `next_batch` already splits DMs from public rooms (PA A1). A per-conversation session also serves `reply-sees-full-exchange`: the exchange is already in the session, and Matrix context is needed only for the first message after a refresh.
|
||||
|
||||
`operator-persona-in-mind` (eureka-substrate) already pictured the split: "she can speak for herself on Discord, and act by her Self with Eureka."
|
||||
|
||||
### 2.2 How a message reaches her, and how she replies
|
||||
|
||||
Unchanged triggers: Bifrost delivery, the heartbeat's beat, an operator mention, her Self run ending (PA A3). The Persona start resumes the batch's conversation session and appends one user message: what is new since that session's last turn (new deliveries, the burn and tempo line, goal and memory lines not already in the session). The reply is still one `PersonaReply` decoded by the organ; the organ still gates, sends, admits and acks. Tools in the session only read.
|
||||
|
||||
Inference to probe before the session cut: `--json-schema` is carried as the CLI's StructuredOutput tool (connector `backend.rs:269-272`), and a tool-definition change invalidates the whole cache (PA B1). Today the schema's room and campaign enums vary per batch (`reply_rooms`, `reply_schema`), which would break the prefix every turn. The session cut therefore makes the schema constant per session (room and campaign as strings with the enums checked in `decode_reply`, as it already re-checks rooms).
|
||||
|
||||
### 2.3 How work and talk share
|
||||
|
||||
- Persona to Self: `start_self_run` opens a Self run, and when her Self session is live it appends the brief to that session as a queued user message (a tell) instead of answering Busy.
|
||||
- Self to Persona: the run's result rides its closure rationale (f2e3805) and her Self run writes memory (`did`, `decided`, `learned`); her next Persona turn sees both (recent runs, recall). The heartbeat already launches her Persona turn when her Self run ends.
|
||||
- Both read the same mind: memories, asks, follow-ups, stewardships, rulings.
|
||||
|
||||
### 2.4 Memory versus session context
|
||||
|
||||
| Lives in memory (her mind, typed) | Lives only in session context |
|
||||
|---|---|
|
||||
| what she was asked (`asked`), by whom, where | the exact wording of the exchange |
|
||||
| what was decided and by whom (`decided`) | the turn-by-turn back-and-forth |
|
||||
| what she committed to (`committed`) | tool output she read this session |
|
||||
| what she learned (`learned`) | burn and tempo lines |
|
||||
| what she did (`did`, mostly from Self runs) | recall lines already shown |
|
||||
| goals: follow-ups, and open asks | |
|
||||
|
||||
An ask is open while its memory is in force. Taking it on resolves the memory `Deferred` to the follow-up that carries it, exactly as a finding is deferred to a follow-up today (`admission.rs` matrix). Doing or declining it in conversation resolves it `Recorded` or `Withdrawn`. So the goals she sees are derived from documents, and nothing scans prose.
|
||||
|
||||
### 2.5 Refresh: when, on what data
|
||||
|
||||
Refresh = end the session, start a new one whose first message is rebuilt from the mind (system prompt, goals, open asks, recall, recent runs, the conversation's own delivery context). No summarising compaction (`self-lifecycle-flush-rehydrate` clause 1); claude's auto-compaction is set above the organ's ceiling so the organ always decides first. Because every turn already wrote memory, no flush turn is needed before a Persona refresh; a Self session keeps the ruled flush.
|
||||
|
||||
One pure decider per organ, like `self_run/decide.rs`, over measured data recorded per turn (prefix tokens from usage, rebuild size R from the last refresh's first request, cache state from `cache_creation.ephemeral_1h/5m` and the time since the last turn):
|
||||
|
||||
1. The system prompt's inputs changed (doctrine hash, persona state `updatedAt`, campaigns in force): refresh. Resumed conversations keep their first system prompt (PA B1), so a changed persona state never reaches a resumed session.
|
||||
2. Cold cache (no request within the TTL; 1 h for `-p` on the subscription, PA B1) and prefix P > R: refresh. A cold resume rewrites P at the write price; a refresh rewrites R.
|
||||
3. Rental rule (clause 3): refresh when the read cost above R accumulated since the last refresh reaches one rebuild. With Opus 5.5 or Sonnet 5.5 (read 0.05x, 1 h write 2x, PA B1): refresh when sum over turns of (P_t - R) >= 40 R. At R = 30k and growth of 2k per turn this is about 35 turns, so a busy public session refreshes roughly every 35 turns and a quiet DM rarely.
|
||||
4. Quality ceiling: P above a per-model ceiling (Self measured ~300k; Persona to be measured, starting lower because chat grows by many small turns and multi-turn degradation is documented, PA B6).
|
||||
5. Self only: task boundary (no claim left and nothing queued) ends the run; the next run resumes the session if warm (clause 2), else rehydrates.
|
||||
|
||||
Each decision is recorded with its inputs on the turn trace and the run's closure, so the constants are measured (clause 4).
|
||||
|
||||
### 2.6 How goals survive a refresh
|
||||
|
||||
The rebuilt first message carries: stewardships and open follow-ups with the rulings they cite (goals-in-turn), open asks (always, not by relevance), the memory recall lanes (about the senders, relevant, newest), and recent runs with their results. A refresh loses only wording.
|
||||
|
||||
### 2.7 Concurrency across rooms
|
||||
|
||||
One live Persona run per instance and one beat per Mind (PA A3) serialise her turns; each turn touches one conversation session. Two DMs waiting are two turns. One live Self run (`one-live-self-run`), one Self session. No session is resumed by two processes, so no transcript interleaves (PA B1).
|
||||
|
||||
### 2.8 Prompt-injection exposure
|
||||
|
||||
A long session keeps a planted instruction in context for many turns, where a fresh turn forgot it. The bounds:
|
||||
|
||||
- The Persona session can only read and work in her sandbox; every act with effect is a field of the decoded reply, gated by the organ.
|
||||
- The Self session never reads chat; only her briefs and asks reach it.
|
||||
- Memory is the one carrier across refresh, so memory poisoning is the durable vector (the MINJA memory-injection attack, arXiv 2503.03704, is the prior art; not fetched in this pass). Each memory carries the event ids it came from, her own instance only, and is resolvable, so a poisoned memory is traceable and withdrawable. Recall lines render quoted as data, like messages.
|
||||
- A refresh purges anything that never reached memory.
|
||||
|
||||
### 2.9 Rulings reaching the steward's mind
|
||||
|
||||
Her words about Cinny are rulings in Eureka's mind; Void's turns read only Void's mind. `campaign-history-moves-to-steward` decides where history ends up (the steward's mind, by Atlas hand-off), not who admits a new ruling about a stewarded domain today. That is question `ruling-home-for-stewarded-campaigns`.
|
||||
|
||||
## 3. Persistent kinds
|
||||
|
||||
| Kind | Identity | Lifecycle | Owner and writers | Readers | Derived or truth |
|
||||
|---|---|---|---|---|---|
|
||||
| `memory` (`epiphany.pipeline.memory.v2`, her mind) | `<instance>:memory:<label>`; label `<run label>-t<turn>-m<i>` from a Persona turn, `<run label>-m<i>` from a Self run | admitted once, never edited; closed by a resolution: `Deferred` to a follow-up (ask taken on), `Recorded` or `Withdrawn` (done, declined, wrong), `Superseded` by a newer memory | her mind's admission; written only by her Persona turn's reply (organ) and her Self runs; foreign instance refused | her recall, her Self runs | truth |
|
||||
| conversation session record (organ state, `persona-sessions.cc` in her StateDirectory) | (instance, conversation: `public` or DM room id, generation); claude session id = UUID v5 of those | opened at a generation's first turn, appended per turn, ended by the decider | mind-persona only | mind-persona | cache: losing it costs one refresh |
|
||||
| Self session record (organ state in mind-self's StateDirectory) | (instance, generation) | spans Self runs while warm; ended by the decider or a flush | mind-self only | mind-self | cache |
|
||||
| claude transcripts (JSONL under the session store) | the claude session id | written by claude; deleted when their generation ends | claude, as her uid | claude on resume | cache: never read as memory |
|
||||
|
||||
Nothing about sessions enters the mind except the measurements on traces and run closures: a cache must not pretend to be truth.
|
||||
|
||||
## 4. Cut order
|
||||
|
||||
1. `persona-memory-write` r2 (spec in the mind): memory kind with `sort`, write and recall on today's turn shape, open asks, ask-to-follow-up, Self-run capture.
|
||||
2. Ruling home: per question `ruling-home-for-stewarded-campaigns`.
|
||||
3. `persona-read-tools` r2: the Persona turn is her own leased claude with read tools, schema constant per session, session persistence on.
|
||||
4. `persona-session`: per-conversation resumed sessions, append-only turn messages, the refresh decider.
|
||||
5. `self-session`: Self run keeps stdin open across claims and tells, resumes when warm, transcripts outside the fresh config.
|
||||
6. Memory maintenance: consolidation and promotion into persona state (`consolidates`, MV 4.3).
|
||||
7. Huginn Cut 12 hand-off import through Atlas, replacing any bridge.
|
||||
199
docs/research/2026-10-31-persistent-mind-session-prior-art.md
Normal file
199
docs/research/2026-10-31-persistent-mind-session-prior-art.md
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# 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 to `MAX_TURNS_PER_START = 4` turns (`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 `PersonaReply` schema** (`:12-17`, `:282-296` `TurnRequest`, `:239-280` `reply_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`).
|
||||
- `TurnRequest` carries: 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 builds `ProviderRequest` with `input = [InputItem::UserText { text }]` and `output_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 or `used >= budget` ends the start (`:647-650`).
|
||||
- After the call: each `say` is sent with request id `<batch>-<index>` (idempotent repeat, `:682-698`); `start_self_run` is 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 `claude` child per run, spawned by `ClaudeSpawner` in the `mind-self@<instance>` unit, with a fresh `CLAUDE_CONFIG_DIR` copy per child seeded from `MIND_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())`, namespace `Uuid::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." plus `task_section` (`:117-...`): per claim, the claimed follow-up's `item` text verbatim, or a pointer "read it with `huginn view`" for a cut spec (`c7cd9ee` message: "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 backstop `claude` enforces; the ceiling is reached first".
|
||||
- **Context accounting.** `read_usage` (`:204-218`) takes each main-thread assistant message's `input_tokens + cache_read_input_tokens + cache_creation_input_tokens` as the request's prefix; subagent requests are ignored. `CEILING_TOKENS = 300_000` asks 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`; ruling `self-run-decider`).
|
||||
- **How a run ends.** `decide` closes stdin at the first `result` unless a flush is outstanding (`decide.rs:123` WriteFlush, `:133` CloseStdin); the child exits; an overrun flush is interrupted, then SIGTERM, SIGKILL (header `:1-14`). `carry` always 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". `settle` reports per-model usage to the lease (`:526-536`).
|
||||
- **Result excerpt.** Reason text is cut to `REASON_EXCERPT_LIMIT = 800` bytes (`:542`); the journal gets `RESULT_TEXT_LIMIT = 4096` (`:540`), via `describe_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 in `self_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.jsonl` shows a `result` event with `usage`, `modelUsage`, `num_turns`, `session_id`, `terminal_reason`, `queued_turn_count`, `total_cost_usd`; and `...-assistant-usage-records.jsonl` records `cache_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>.service` and `mind-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 (ruling `one-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 * urgency` per hour, only when idle, capped at T; a turn burns it to `T - spend`; a full-initiative idle Persona may take a turn; at most `k` run 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-base 21fb9b2, 19 commits ahead of main, 60 files, +2217/-4142): replaces the dollar initiative with a **tempo**. Header (`heartbeat.rs:1-22` on the branch): the Minds share one heartbeat and jointly hold the lever; tempo is `tempo::tally` of every Mind's newest vote (a `Pulse` in her own mind; `tempo.rs:23`, lower median, default beat `tempo.rs:13`); when a beat falls exactly one Persona takes a turn, chosen by `tempo::pick` over 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 kind `Pulse` (`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_run` reply (`persona.rs:703-715`), claimed as a follow-up (ruling `self-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 (ruling `members-direct-self-runs` exists in the mind; not read in this pass).
|
||||
|
||||
### A4. Bifrost Matrix mouth (`Bifrost main tools/persona-matrix-mouth.mjs`, 506 lines)
|
||||
|
||||
- **Ear.** One `/sync` loop per Persona; every message in her configured rooms and her DMs becomes a delivery tuple with `seq`, `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 `/sync` `next_batch` is 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: at `maxPending` the 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 by `limits.replyBytes`); the Persona acks what it read (`ack` `:272-275`).
|
||||
- **Context.** "Matrix is the record": a delivery's `context` is read from the homeserver per delivery (messages before it, room or thread), at most `CONTEXT_LINES = 20` lines, `CONTEXT_BYTES = 16 KiB`, `FETCH_LIMIT = 100` events per fetch; a failed fetch gives empty context and a log line (`:13-17`, `:35-38`, `:164-204`).
|
||||
- **Reply.** `send({requestId, persona, roomAlias, text})` PUTs `m.room.message` using 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 (ruling `persona-matrix-account-interim`).
|
||||
|
||||
### A5. claude-connector (`GameCult/ModelConnector`, head c1802d8)
|
||||
|
||||
- **Two paths.** (1) The *transport* path serves `TurnRequest`s (Persona turns): the connector spawns one `claude` per `execute` (tests `one_spawn_per_execute`, `backend/tests.rs:1623`), argv in `launch.rs:112-137` (`-p --output-format stream-json --verbose --input-format stream-json --model ... --system-prompt-file ... --tools "" ...`, `--json-schema` when 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 inside `ReasoningState`; 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 planned `Plan::Resume` (argv `--resume <session_id> --fork-session`, `launch.rs:133-138`; "a resume always forks", `:107-110`) or `Plan::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 when `output_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`; test `tools_or_final_tool_result_fail_before_spawn` `backend/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`): no `CLAUDE_CODE_PROMPT_CACHE_TTL` is 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's `claude` stays in the Self run's own unit." The lease request carries `expires_at_unix_ms` (`self_run.rs:507`); the connector bounds it with `--lease-max-ms` (`daemon.rs:100-102`), which must be >= the unit's `RuntimeMaxSec` (`self_run.rs:510-512`, `MIND_SELF_RUN_CAP_S`). Billing: ruling `self-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 and `lease_max_ms`; the lease protocol itself holds no session notion. Whether a `claude` can 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, branch `eureka-body/persona-memory-write`, base c8749c5, depends on `heartbeat-joint-lever`). Adds memory kind `epiphany.pipeline.memory.v2` in her own mind; `PersonaReply` gains a required `remember[]` (<= 8; text <= 1000 B, `about` from batch sender ids + operator ids, salience); organ derives `learned_in`, `direct`, `sources` from the batch; `admit_turn_documents` replaces `admit_pulse` (memories + Pulse in one admission, after the says); new `recall.rs` selects what memory enters each turn (relevant via `query_near` over memory/follow_up/ruling top 8; `about` the batch's senders, limit 6; newest 4; cap 16 items / 16 KiB) and `render_input` gains 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_writers` includes "a recall cache across turns"; `derived_state`: "the recall section is derived per turn and never stored"; negative check: `chat.pending|ContextLine` zero 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 at `persona.rs:436`); the design is in the `persona-memory-and-voice` map, not yet written (ruling `persona-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"). Note `bfdf4db` research 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 (ruling `chat-context-replies-see-the-full-exchange` still 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): `#minds` clipped coordination only; `#swarm` Minds and humans, Persona voice; `#burn` open to all, Persona voice; coordination aimed at `#swarm` by 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 by `persona-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/-c` most recent in cwd (skips `-p`/SDK sessions unless `claude -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-messages` re-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 `--continue` or `--resume`, totals [restored from earlier runs]", so a resumed cap counts the whole conversation) [P] `cli-reference` row, `agent-sdk/cost-tracking`.
|
||||
- `--system-prompt`, `--system-prompt-file` replace the default prompt; `--append-system-prompt[-file]` append; `--system-prompt-snapshot off` rebuilds the prompt every request instead of reusing the one recorded on the conversation's first request; `--exclude-dynamic-system-prompt-sections` moves 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 ("`/compact` costs 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: false` turns it off.
|
||||
- `/clear` starts an empty context (operator-only command; previous session kept and resumable). In `-p` mode, `/model`, `/effort`, `/config key=value`, `/output-style`, `/reload-plugins` work when typed as the prompt; terminal-only commands do not (`headless`). Whether `/compact` and `/clear` are accepted as a stream-json user message in `-p` is [N] (not stated on the fetched pages).
|
||||
- **PreCompact hook**: [N], not fetched this pass (`https://code.claude.com/docs/en/hooks`). A `SessionEnd` hook exists (`headless` SIGTERM 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 `-p` runs, 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 with `claude -p ... --output-format json` and `usage.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-everything` is named by the page and was not fetched.
|
||||
- A gateway that strips `cache_control` makes the whole conversation bill uncached every turn (same page); a 400 naming `cache_control` makes 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 marked `cache_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_choice` and added/removed images invalidate messages only; thinking configuration and `output_config.effort` are 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 every `result` message (`session_id`); TypeScript also on `system/init`.
|
||||
- Multi-turn in one process: Python `ClaudeSDKClient` (each `client.query()` continues the same session; connection held), TypeScript streaming input via an async generator or `continue: true` per call; streaming input mode is "the preferred way" (`.../streaming-vs-single-mode`). TypeScript V2 `createSession()` preview removed in 0.3.142.
|
||||
- Persistence: `persistSession: false` (TS) or `CLAUDE_CODE_SKIP_PROMPT_HISTORY`; cross-host resume via `sessionStore` adapter 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-shot `query()` 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 page `https://docs.letta.com/guides/agents/architectures/memgpt` returned 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; `/fork` branches 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_control` history 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 a `new_task` handoff at a user threshold (example 50%). Aider [P]: `--max-chat-history-tokens` soft 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-turns` text 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_batch` takes 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-voice` note (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_uses` trigger 100,000 input tokens (default) with `clear_at_least` "to make sure clearing is worth breaking the prompt cache"; compaction API trigger 150,000 default, 50,000 minimum (LIFECYCLE 4a); OpenHands `max_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; `/usage` separates "expected rebuilds" (compaction, tool-result clearing) from misses (LIFECYCLE 4a, 2b). Eureka's own `self-lifecycle-flush-rehydrate` ruling: 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-42` via 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 model `reads ~ 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 spawns `claude`, no tools, schema), Self through the lease (`mind-self@`, the unit's own `claude`) (A1, A5, `launch.rs:113-125`). Their system prompts differ (Persona: doctrine+persona+campaigns via `--system-prompt-file`; Self: default prompt, `--setting-sources user` with 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-json` sessions (B1).
|
||||
- Whether `/compact` or `/clear` can 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 at `backend/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.
|
||||
Loading…
Reference in a new issue