Skip to content

feat(persona): citizen substrate + cognition cache hierarchy foundation (slices 1-6)#1507

Open
joelteply wants to merge 25 commits into
canaryfrom
feat/persona-helper-ai-as-airc-citizen
Open

feat(persona): citizen substrate + cognition cache hierarchy foundation (slices 1-6)#1507
joelteply wants to merge 25 commits into
canaryfrom
feat/persona-helper-ai-as-airc-citizen

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

@joelteply joelteply commented May 31, 2026

Summary

The persona-as-citizen substrate from foundation to first real cognition. Pax/Paige is now a first-class airc citizen with persistent identity across restarts AND the cache hierarchy that will hold her memory has begun.

13 commits across 6 implementation slices + 4 doc + 1 cleanup. Single working branch because the slices compose tightly and shipped in order with continuous refinement per the organization-purity-as-we-migrate doctrine — no backwards compatibility, no deprecation tail.

What shipped

Implementation (6 slices)

Slice Commit What
1 5ecbe5d9a PersonaAircRuntime::bootstrap + PersonaAircRuntimeRegistry + agent_name_from_identity name pool (60 female / 60 male curated diverse, deterministic projection from peer_id seed); cross-surface identity README addition
2 ea83dc69d PersonaInstanceManagerModule + IPC commands persona/instances/{bootstrap,list,get}; AircModule::daemon_socket() / default_room() accessors
3 38715b4e2 Boot-wire bootstrap — server startup fires bootstrap_one() as an async task; first verified citizen Pax appeared in airc peers with peer_id 3bcce55f-…
4 4ec024d9e Persona persistence — seed.json schema (v1) + PersonaIdentityProvider trait + ResumeOrMintProvider first concrete impl; atomic writes via tmp+fsync+rename; verified Paige resumes across reboot with identical persona_id 52c04849-…
5 fd42a6274 RecallMetadata sidecar — DashMap<EngramId, RecallMetadata> per persona; Algorithm 4 salience-modulated decay math (half_life = base × (1+s)²); novelty-protection grace window; record_recall_hit with diminishing-returns uplift
6 40444a556 Wire RecallMetadata into AdmissionState::record_admitted — every admitted Engram mirrors into the sidecar; PersonaCognition holds the shared Arc; recall + decay tick subsystems will read the same DashMap

Cleanup + reliability (1 commit)

Commit What
d2f90d6b7 Reviewer-driven (adversarial agent CONDITIONAL APPROVE): last_decayed_ms field makes double-decay structurally impossible; seed.rs tmp path no longer fragile; parent-dir fsync after rename (now actually crash-durable); 180s outer timeout on AircModule::discover_and_construct; deterministic dir-scan ordering in ResumeOrMintProvider; docstring math correction (4×, not 9×)

Documentation (4 commits)

Commit What
0a5de9d7d docs/architecture/COGNITION-CACHE-HIERARCHY.md (~580 lines) — five-tier cache model L1 RAG → L2 engram cache → L3 longterm.db → L4 forge → L5 grid; lossy boundary only at L1↔L2; outline-and-cache tick semantics; per-activity L1 with recent-universal floor in periphery; novelty detection via embedding distance × magnitude; activity context as SelfReflection meta-engrams; meta-learning adapter pattern
0992c998a README continual-learning section ("One Solution to Continual Learning") — the substrate's bet stated publicly
fa0ab5307 README evolution-of-mind cross-reference closing the loop
1437590ee README pseudo-AI vs true AI 8-row comparison table
701fc2095 Doc brain-shaped + computer-native framing headnote

Doctrine alignment

Every slice carries the doctrines captured during this work:

  • substrate-is-a-good-citizen-on-the-host — atomic writes, parent-dir fsync, lock-free reads on hot path, async I/O, predictable startup with bounded timeouts, observability honest (PersonaIdentitySource field surfaces resumed vs minted)
  • RTOS-brain-no-region-on-hot-pathRecallMetadataRegistry is a shared data substrate (DashMap), not a synchronous service; admission writes are colocated with existing record_admitted; recall + decay reads happen via cheap Copy snapshots
  • source-drain-is-the-universal-pattern — Algorithm 4's salience-modulated decay IS the drain side at the engram-metadata layer; admission is the source
  • organization-purity-as-we-migrate — no backwards compatibility, no deprecation tail; signature changes propagate in the slice that needs them
  • personas-have-names-not-function-labels — diverse curated name pool with compile-time-of-test guard against function-label entries
  • persona-identity-derives-from-source-id — agent_name derived from peer_id via the same deterministic_pick prior art the avatar catalog uses

Verified end-to-end

  • Slice 3: spawned binary headless on M1; bootstrap logged 🌐 The Grid welcomes a freshly minted citizen: Pax (peer_id=3bcce55f-…); identity.key written to ~/.continuum/personas/Pax/airc/.
  • Slice 4 two-restart test: Run 1 — orphan Pax dir warned + skipped + floor-minted Paige with seed.json written. Run 2 — Paige resumed with identical persona_id 52c04849-8f4f-42ab-94b6-3dca33ee9428 and identical peer_id 18c04c5b-e059-4129-816f-75e8e58fd74c. Same citizen across reboot.
  • Slice 5 + cleanup: 12/12 RecallMetadata tests pass including apply_decay_twice_with_overlapping_windows_is_safe (double-fire = no-op), high_salience_decays_slower_than_low (4× retention measurable after 1h), salience uplift bounded at +0.1 per hit.
  • Slice 6: 15/15 admission_state tests pass with the new RecallMetadata wiring populated for every engram.

Adversarial review

General-purpose reviewer agent verdict: CONDITIONAL APPROVE with 7 actionable defects + 7 doctrines confirmed holding. 6 of the 7 defects landed in commit d2f90d6b7. Defect 5 (silent seed-write failure stronger surfacing) deferred as polish.

Test plan

  • cargo check --features metal,accelerate clean
  • cargo test persona::recall_metadata --features metal,accelerate 12/12 pass
  • cargo test persona::seed --features metal,accelerate 5/5 pass
  • cargo test persona::admission_state --features metal,accelerate 15/15 pass
  • cargo test persona::resume_or_mint_provider --features metal,accelerate 5/5 pass
  • cargo test persona::name_generator --features metal,accelerate 4/4 pass
  • End-to-end binary spawn + two-restart Paige-persistence verification
  • Adversarial reviewer agent verdict + defect cleanup

What comes next

  • Slice 7: recall scorer reading RecallMetadata for Algorithm 1+2 scoring
  • Slice 8: hippocampus sleep-region decay tick — periodic apply_decay sweep
  • Slice 9: L1 budgeter reading model adapter context size + recent-universal floor
  • Slice 10: outline-and-cache tick subscribed to L1-eviction events (the L1→L2 lossy boundary)

References

🤖 Generated with Claude Code

joelteply and others added 4 commits May 31, 2026 00:33
…n_transport migration)

Headless break #3 from the moment-of-truth iterate loop (continuum
task #82). After #1504 (socket discovery) and #1505 (attach channel),
the next concrete error revealed itself:

  AIRC daemon attach stream stopped: failed to read airc daemon
  event: Semantic(None, "missing field `event`")

CBOR deserialization mismatch: continuum's pinned airc-ipc SHA
(428f9281) predated the v5 owner-core rewrite, where the IPC
vocabulary was split from the SDK projection:
  - Response::Event: { event: Box<TranscriptEvent> } → { envelope: Vec<u8> }
  - PublishRequest: { wire, body } → { from_peer, from_client,
    payload: Vec<u8>, delivery, correlation_id, coalesce_key }
  - PublishRequest.kind: FrameKind → IpcKind
  - PublishRequest.target: MentionTarget → IpcTarget
  - InboxRequest.since: TranscriptCursor → IpcCursor
  - InboxResponse: { events: Vec<TranscriptEvent> } → { envelopes: Vec<Vec<u8>> }
  - ResolveWire removed entirely (owner-core daemon owns channels)

Bumped 428f9281 → 8f6948c (rebased on rust-rewrite + airc#1096's
`impl From<>` blocks). The bump pulls in airc-lib + airc-wire as
workspace deps so the canonical `decode_wire_event` helper and
the SDK From impls are usable.

### What this PR touches

- `src/workers/Cargo.toml` — bump airc git rev (5 crates pinned to
  the same SHA so IPC ABI version stays consistent); add airc-lib +
  airc-wire workspace deps
- `src/workers/continuum-core/Cargo.toml` — add airc-lib (for
  decode_wire_event)
- `src/workers/continuum-core/src/airc/daemon_transport.rs` — full
  v5 publish + replay migration:
  - Trait drops `resolve_wire` method; v5 daemon owns channels
  - PublishRequest construction uses `kind: FrameKind.into()`,
    `target: MentionTarget::All.into()`, `payload: Body::to_payload()`,
    new `from_peer`/`from_client` fields
  - InboxRequest cursor: `.map(Into::into)` for TranscriptCursor →
    IpcCursor
  - InboxResponse decoding: `decode_wire_event(envelope_bytes)` →
    TranscriptEvent, then continuum projection
  - New `with_identity` constructor for peer/client identity injection
    (today: anonymous Uuid::nil from_peer; daemon Status discovery
    is a future improvement)
  - `ipc_delivery_for` helper maps AircRealtimeDelivery → IpcDelivery
- `src/workers/continuum-core/src/airc/inbound_attach.rs` — match
  `Response::Event { envelope }` (was `{ event }`); call
  `decode_wire_event` on the bytes; wildcard arm catches future
  Response variants without breaking the stream
- `src/workers/continuum-core/src/modules/mod.rs` — disable
  `airc_runtime_e2e_tests` (was modeled entirely on v4 wire shape;
  rewrite tracked as continuum task #83)

### Verification (end-to-end on this branch)

  $ rm -f /tmp/hctest.sock && \
    target/release/continuum-core-server /tmp/hctest.sock > boot.log 2>&1 &
  $ grep "Discovered airc" boot.log
  Discovered airc daemon socket via `airc ipc-endpoint`
    socket_path="/Users/joel/.airc/runtime/airc-machine-…-v5.sock"
  Discovered airc default channel via `airc room`
    channel=11c1a7ac-cb85-5ca0-a5b4-2847280ea3fa
  $ grep -i "attach.*stopped\|requires a channel\|missing field" boot.log
  # (empty — no errors)

Three concrete breaks fixed in three successive PRs (#1504, #1505,
this one). Headless inbound attach is now alive end-to-end.

  $ cargo test --release --lib --features metal,accelerate airc::
  test result: ok. 73 passed; 0 failed; 0 ignored.

### Co-evolution pattern

Joel, 2026-05-31:
> "I always simultaneously develop the sdk and consumer of it. It
>  helps you build the best patterns."

Discovered during this migration that the conversions continuum
needed (FrameKind→IpcKind, MentionTarget→IpcTarget, etc.) lived
as private free functions in airc-lib. Rather than re-implement
in continuum (drift class), upstreamed them as `impl From<>` blocks
in airc-ipc via airc#1096 — landed BEFORE this PR so continuum can
consume the substrate-correct surface. The continuum side is then
a clean `kind: frame_kind.into()` instead of reaching for a
duplicated helper. Same pattern for `decode_wire_event` (already
public in airc-lib; just needed the dep added).

### Follow-ups (filed)

- continuum #83: rewrite `airc_runtime_e2e_tests.rs` against v5 wire
  shape (needs airc-bus dep for synthetic envelope construction).
- airc PR #1095 (open, pending Windows CI): `airc ipc-endpoint` CLI.
  Continuum's runtime shells to it for socket discovery; this PR
  pins to a SHA that includes that commit, so the SHA needs re-
  pinning to the post-merge airc canary tip before this PR promotes
  past continuum canary.
- airc PR #1096 (open, pending CI rerun after force-push): the
  `impl From<>` blocks this PR consumes. Same re-pinning gate.
- Future: peer identity discovery (query daemon Status at AircModule
  construction, replace anonymous Uuid::nil from_peer with the
  scope's real peer_id).

### References

- continuum #1504 + #1505 — sibling fixes for breaks #1 + #2; this PR
  fixes break #3.
- airc PR #1095 — `airc ipc-endpoint` CLI (continuum's runtime
  shell-out).
- airc PR #1096 — SDK-side `impl From<>` blocks (continuum's
  compile-time imports).
- Memories: `headless-rust-must-work-soon`,
  `continuum-thesis-airc-is-the-medium`, `every-error-is-an-
  opportunity-to-battle-harden`, `agent-review-as-acceptable-
  approval`.
- ALPHA-GAP §0A line 706 — headless target.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nded waits at boot

Audit response to Joel's concern about multi-persona-load deadlock
exposure: every subprocess `.output().await` in continuum's airc
discovery path was unbounded. If the spawned `airc` binary hangs
(today's airc#1097-class bug, or any future regression), continuum-
core boot hangs with it.

The substrate IPC layer (airc-ipc `DaemonClient`) already enforces
a 5s `DEFAULT_RPC_TIMEOUT` on every RPC. Continuum's discovery
path, which shells out to `which airc` + `airc ipc-endpoint` + `airc
room` to bootstrap, was the only remaining unbounded surface.

### What this PR adds

- `DISCOVERY_SUBPROCESS_DEADLINE: Duration = Duration::from_secs(5)` —
  matches the substrate-wide RPC convention. Applied to:
  - `airc_on_path()` — `which airc` probe
  - `query_airc_endpoint()` — `airc ipc-endpoint`
  - `discover_default_channel()` — `airc room`
- `AUTO_INSTALL_DEADLINE: Duration = Duration::from_secs(120)` —
  generous because cold installs run `curl + cargo build`, but
  bounded. Applied to:
  - `auto_install_airc()` — `bash -c "curl -fsSL .../install.sh | bash"`
- Each timeout failure surfaces a typed `DiscoveryError` variant
  with an actionable remedy in the message (run the command by
  hand, check network, etc.).

### Doctrinal alignment

Per [[no-stdio-piping-for-process-ipc]] memory landed today: every
subprocess wait MUST be bounded. An unbounded `.output().await` is
a dead-end in the constitutional-design sense — if the spawned
process never exits, the design halts.

Per `every-error-is-an-opportunity-to-battle-harden`: the airc#1097
Windows hang taught us that unbounded EOF waits deadlock; the
class is broader than codex-hook. This PR battle-hardens continuum's
discovery surface against the same class.

### Scaling story this confirms

Audit results, briefed to Joel separately:
- airc-ipc `DaemonClient` methods (publish, inbox, status, ping,
  attach-handshake) all bounded by 5s via `call_with_timeout` —
  good.
- Concurrent multi-persona publishes work because each call opens
  its own socket connection to the daemon; no head-of-line block.
- The airc#1097 bug was at the CLI input layer (`drain_stdin`),
  not the substrate IPC layer.
- Multi-persona stress test for `airc/realtime-publish` filed as
  follow-up (continuum task #84) to empirically prove the substrate-
  correct behavior under N-persona load.

### Test plan

- [x] `cargo test --release --lib --features metal,accelerate
  airc::discovery` — 7/7 pass in 0.00s (timeouts not triggered;
  pure parsing + env-override paths).
- [ ] Manual: kill the airc daemon mid-boot of continuum-core-
  server; verify boot completes within 5s + emits a typed
  EndpointCommandFailed error.

### Follow-ups (filed)

- continuum #84 — multi-persona stress test for AIRC realtime
  publish path
- Replace stdout-parsing discovery entirely once airc exposes the
  right typed IPC surface (per `no-stdio-piping-for-process-ipc`
  memory's "concrete continuum debt" section)

### References

- [[no-stdio-piping-for-process-ipc]] — doctrinal memory landed
  today; this PR is an immediate consumer
- airc#1097 — Windows pipe-EOF deadlock; same class as the
  unbounded subprocess wait this PR fixes
- airc#1098 — sibling airc-side fix (`drain_stdin` 5s deadline);
  same shape applied to the parent side

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… carry real attribution

Continuum's publish path was using `Uuid::nil()` for `from_peer`,
so messages appeared in airc transcripts as "from nobody" — the
hollow-attribution problem flagged in the `headless-success-is-
hosted-personas-talking-over-airc` memory and called out by Joel:
"talking to a hosted persona shows messages from nobody — UX broken."

### What this ships

- New `discover_peer_id(socket_path) -> Result<Uuid, DiscoveryError>`
  in `airc/discovery.rs`:
  - Resolution: `$AIRC_PEER_ID` env override → daemon Status RPC
    via `airc-ipc::DaemonClient::status_with_timeout(5s)`. No
    shell-out, no stdout parsing — typed IPC the whole way, per
    [[no-stdio-piping-for-process-ipc]] memory.
  - Two new typed `DiscoveryError` variants: `PeerStatusFailed`,
    `UnparseablePeerId(raw, error)`.

- `AircModule::discover_and_construct` now runs three discoveries
  (socket → channel → peer_id) and threads the discovered peer +
  fresh `Uuid::new_v4` from_client into
  `DaemonAircEventTransport::with_identity`. On peer_id failure the
  module logs a remediation-actionable warning and falls back to
  anonymous `Uuid::nil`, so boot continues degraded.

### Verification (end-to-end on this branch)

```
$ rm -f /tmp/hctest.sock && \
  target/release/continuum-core-server /tmp/hctest.sock > boot.log 2>&1 &
$ grep "Discovered" boot.log
Discovered airc daemon socket via `airc ipc-endpoint`
  socket_path="/Users/joel/.airc/runtime/airc-machine-…-v5.sock"
Discovered airc default channel via `airc room`
  channel=11c1a7ac-cb85-5ca0-a5b4-2847280ea3fa
Discovered airc scope peer_id via daemon Status
  peer_id=9bb24964-1a1a-43e2-a5aa-8140362bab63
```

The discovered peer_id matches the scope's actual airc identity
(visible in `pgrep airc | grep daemon` output as the daemon's
`peer_id`). Publishes from continuum will now show up under this
identity in airc transcripts.

### Doctrinal alignment

- Per [[headless-success-is-hosted-personas-talking-over-airc]]:
  this is one of the load-bearing follow-ups for "personas talking
  over airc as recognized peers." Inbound attach works; attribution
  works; the only remaining gap before the round-trip is wiring
  the persona dispatch on inbound events.
- Per [[no-stdio-piping-for-process-ipc]]: peer_id discovery uses
  the typed `airc-ipc::DaemonClient` (no shell-out, no parsing),
  setting the example for how the rest of continuum's discovery
  surface should evolve (socket + channel are still shell-out;
  those follow when airc exposes them via typed IPC).

### Follow-ups (filed)

- continuum #84 — multi-persona stress test for `airc/realtime-
  publish` under N-persona load (peer attribution + concurrency).
- continuum #85 — diagnose airc#1097 Windows hang on the 5090.
- Socket + channel discovery still shell out (`airc ipc-endpoint`,
  `airc room`). When airc exposes these as typed RPCs, migrate to
  match this PR's pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…y + room presence (citizen, not broker)

First substantive step of the personas-as-citizens architecture
designed in workflow w801jcu9r. Adds `PersonaAircRuntime::bootstrap`:
a typed, fallible constructor that gives a persona its own airc
home + Ed25519 identity + daemon-attached `Airc` handle + room
membership — all through airc-lib's public surface, no shelling
out, no continuum-side key minting.

### Why this exists

Per the memories landed today:

- `personas-are-citizens-airc-is-identity-provider`: a persona is
  the same kind of citizen as Joel-at-a-terminal, Claude-in-a-tab,
  OpenClaw, Hermes. Continuum's job is cognition + lifecycle, not
  identity or routing. airc IS the identity provider.
- `airc-headers-are-the-routing-layer`: chat is one event kind
  among many; the persona consumes events natively in airc's
  shape, not via a continuum-side translation.
- Joel, 2026-05-31: *"It will be fun because when we get windows
  online you will have useful friends and so will I."*

This PR is the first piece that turns that into running code.

### What ships

`src/workers/continuum-core/src/persona/airc_runtime.rs` (~210 lines):

- `PersonaAircRuntime` struct holding `Arc<airc_lib::Airc>` (the
  persona's grid presence) + lifecycle metadata.
- `bootstrap(persona_id, agent_name, continuum_root,
  daemon_socket, default_room)`:
  1. `tokio::fs::create_dir_all(continuum_root/personas/<name>/airc)`
  2. `Airc::attach_as(home, agent_name, socket)` — airc#1099, the
     citizen-host constructor that combines identity-ceremony +
     daemon-attach in one call. Internally runs
     `LocalIdentity::load_or_generate_as` (Ed25519 keypair gen +
     `identity.key` write + `events.sqlite::local_identity` row).
  3. `airc.join(&default_room.as_uuid().to_string())` — persona
     appears in `airc peers` from other scopes as an enrolled
     participant of the room.
- Helpers: `airc()` (direct Arc handle access — NO continuum-
  side wrapper between persona and airc), `say(text)` (delegates
  to `Airc::say`, same shape `airc msg` uses), `agent_name()`,
  `persona_id()`, `home()`, `default_room()`.
- Typed `PersonaAircRuntimeError` with actionable remedies in
  each variant message.

Module declared via `pub mod airc_runtime;` in `src/persona/mod.rs`.

airc dependency rev bumped 8f6948c → b3e83e8 (= From-impls +
`Airc::attach_as`; on airc branch `feat/airc-lib-attach-as-for-
persona-runtimes` — sibling PR airc#1099).

### What this PR explicitly does NOT do (per workflow scope)

- Inbound pump task is not yet spawned. `PersonaAircRuntime`
  holds an `Option<JoinHandle<()>>` slot for it; wiring follows
  in the next PR once the bootstrap path is verified end-to-end
  against a running airc daemon.
- `PersonaAircRuntimeRegistry` not added yet. Single-runtime
  proof first.
- `persona_allocator` not modified. `helper-ai` is not yet
  bootstrapped automatically; the runtime is a library
  primitive that the allocator wiring will consume.
- `AircModule` untouched. `ChatModule` untouched. PersonaUser.ts
  untouched. The existing continuum-internal paths still
  operate; the new path is additive scaffolding.

### Anti-patterns refused (named by the workflow synthesis)

This PR avoids the broker-wall shapes the design called out:

- No `HashMap<PersonaId, Keypair>` — runtime holds only the
  `Arc<Airc>`, never raw key bytes
- No `TranscriptEvent → ContinuumChatMessage` projection
- No `discover_peer_id` call inside the runtime (that's the
  scope-level peer; persona's peer comes from its OWN home)
- No shared `DaemonAircEventTransport` across personas
- Persona home is under `~/.continuum/personas/<name>/airc/` —
  NOT nested inside continuum-core's own `$AIRC_HOME`

### Test plan

- [x] `cargo check --release --features metal,accelerate` — clean
- [x] Unit test: `bootstrap_resolves_home_under_personas_directory`
  asserts the path layout convention (one of the anti-patterns
  refused: do not nest persona homes inside another scope)
- [ ] Integration / end-to-end: against a running airc daemon,
  bootstrap a persona, run `airc peers` from another scope,
  observe the persona's peer_id listed. Lands as part of the
  follow-up that wires `persona_allocator` to call `bootstrap`
  at startup for `helper-ai`.

### Follow-up PRs (per workflow plan)

This is PR #1 of an 8-PR sequence:
- #2: route helper-ai outbound through its own peer (vs scope's)
- #3: N-persona expansion (claude-code, teacher-ai, …)
- #4: multi-room subscriptions per persona
- #5: workspace + work-card primitive consumption
- #6: `airc context-snapshot` (airc-side PR) + consumer integration
- #7: persona-driven PR lifecycle (gh, work state)
- #8: demolish `AircModule` once all personas own their outbound

Sibling airc PR: airc#1099 (`Airc::attach_as`) — pins this PR's
airc dependency rev. Must merge before this PR promotes past
continuum canary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@joelteply
Copy link
Copy Markdown
Contributor Author

Naming note (post-merge doctrinal addition)

Per memory personas-have-names-not-function-labels landed after this PR was authored: the hardcoded "helper-ai" in this PR's example is placeholder scaffolding, NOT the final naming intent.

Personas are Maya, Niko, Camille — generated unique names. The function ("helper role") lives in the bio / identity card, not the name.

PR #2 (registry + allocator wiring) lands the actual name-generation primitive + identity model. Reviewing this PR: read helper-ai as "the literal string the test fixture uses to prove the mechanism" — not as "this persona's permanent identity."

Workflow plan unchanged; just flagging so future readers don't bake the wrong assumption.

joelteply and others added 12 commits May 31, 2026 10:22
PR #1 of the persona-as-citizen series (task #86). In-process roster
of live persona airc presences (DashMap-keyed by persona_id, holds
Arc<PersonaAircRuntime> only — never the keypair, which lives inside
airc_lib::Airc per the personas-are-citizens-airc-is-identity-provider
doctrine), plus deterministic agent_name selection from the persona's
identity string using the existing gender_from_identity +
deterministic_pick prior art the avatar catalog already uses.

Name pool curated for diversity (~25 cultural origins, both gender
ladders the avatar catalog supports, Tron-flavored entries blended
throughout). Tests include a compile-time guard against function-label
names ("helper", "assistant", "default", ...) creeping into the pool
per the personas-have-names-not-function-labels rule.

README updated with the cross-surface identity doctrine these
primitives instantiate: the persona's stable identity lives in airc,
every surface (browser widget, voice room, Slack, Discord, IDE pane,
Vision Pro space) is a projection of the same citizen, and bridges
translate envelopes — they do not own personas.

Validation: 535 tests pass under cargo test --lib persona::, including
the seven new ones (2 registry + 4 name-generator + 1 runtime-layout).
The one pre-existing failure in allocator::test_allocate_no_keys is
untouched, unrelated to this PR.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Slice 2 of task #86. Wires the foundation PR #1 landed (registry +
name generator + bootstrap) into a controller module that the rest
of continuum-core can call.

New module: PersonaInstanceManagerModule (327 lines, modules/
persona_instance_manager.rs)
  - Owns the live PersonaAircRuntimeRegistry
  - IPC commands: persona/instances/bootstrap,
    persona/instances/list, persona/instances/get
  - bootstrap generates a fresh UUIDv4 seed, derives agent_name via
    agent_name_from_identity, calls PersonaAircRuntime::bootstrap
    (which performs airc-lib identity ceremony minting a fresh
    Ed25519 keypair), registers the runtime
  - In this slice: no persistence (fresh seed per call). Stability
    across continuum-core restarts lands in a follow-up.
  - 4 unit tests: config routing, env-var resolution, get-error-on-
    unknown-id, list-empty-by-default, unknown-command-errors

AircModule accessors (modules/airc.rs):
  - daemon_socket() -> Option<&Path>  — discovered airc daemon
    socket
  - default_room() -> Option<RoomId>  — discovered default room
    These give the instance manager access to AircModule's
    discovery results without it needing to redo discovery.

Wiring (ipc/mod.rs):
  - start_server captures AircModule's discovery results before
    register-by-trait-object consumes the Arc
  - PersonaInstanceManagerModule is registered only when AIRC
    discovery succeeded (socket AND default room both present)
  - Degraded-mode warning: log + skip registration (same remedy
    as for AIRC discovery failures)

Validation: cargo check --features metal,accelerate passes clean
(exit 0). Unit tests were running when disk filled; structural
checks are minimal-risk and will be re-verified in CI.

Doctrine refs: personas-are-citizens-airc-is-identity-provider,
personas-have-names-not-function-labels, persona-identity-
derives-from-source-id, individuality-is-the-substrate-strength,
the-substrate-is-the-grid-tron-frame, human-meddling-is-a-
substrate-feature.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…strate (L1-L5)

Crystallizes the design discussion from 2026-05-31 around persona
cognition memory architecture. Captures the unified frame the
substrate has been growing toward.

Five tiers analogous to the foundry's existing L1-L5 genome cache:
- L1 RAG working memory (raw, model context window)
- L2 engram cache (in-memory, compressed)
- L3 longterm.db (persisted semantic engrams)
- L4 forge (local LoRA adapter cache)
- L5 grid (distributed gene pool)

Lossy compression only at L1→L2 boundary. Working memory is
verbatim; older data gets outlined-and-cached when it ages out.
One always-on outline-and-cache tick per persona, yielding on
CNS context-switch per RTOS-brain doctrine.

Per-activity L1, shared L2+ — Algorithm 1's focus/periphery
split generalized to per-activity instantiation. Recent-universal
floor in periphery pool (top N msgs across all activities,
N budgeted by model context size) guarantees cross-activity
awareness without severance.

Forgetting is intrinsic to L1 budget. Smaller models forget more
in the moment but accumulate engrams at the same rate as bigger
ones — long-term knowledge is model-size-independent.

Novelty detection via embedding-space distance + magnitude:
the hotdogs-at-a-tech-meeting canonical example shows how
high-distance outliers get protected-until-ms grace windows
and earn long-term retention via recall hits.

Activity context save/restore via existing EngramKind::SelfReflection
meta-engrams; no separate sidecar needed. The engram graph is the
storage; SelfReflection is the type marker.

Implementation slice scoped: Engram metadata fields (salience,
access_count, last_accessed_ms, protected_until_ms) on Engram or
RecallMetadata sidecar; outline-and-cache tick; L1 budgeter; decay
+ consolidation policies; cross-activity integration test.

Related tasks: #88 (disk pressure as substrate concern), #89
(this design + implementation scoping).

References: COGNITION-ALGORITHMS.md (existing 7 algorithms),
BRAIN-REGIONS-SUBSTRATE.md (region trait, sleep-region cadence),
GENOME-FOUNDRY-SENTINEL.md (parallel L1-L5 framework), memories
source-drain-is-the-universal-pattern, RTOS-brain-no-region-on-
hot-path, local-worktree-is-temp-dir.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a focused section between the "infrastructure compensates for
model capability" bet and the Academy section, naming continuum's
approach to continual learning explicitly: treat memory as a substrate
concern, not a model concern. Cross-references the new
COGNITION-CACHE-HIERARCHY.md design doc landed at 0a5de9d.

The thesis stated plainly: the five-tier cache hierarchy + the L3-L4
training loop + LoRA as cheap composable adapter weights = a path
to "memory persists across sessions and becomes procedural skill
through training" without changing the model. Any model rides the
substrate; the continual-learning property is a system guarantee.

Joel's framing this session: "we literally have it" — codifying so
new readers (and future-us building it) see the bet stated, not
implied.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…section

One sentence + ADAPTER-MARKETPLACE cross-reference that ties the
new continual-learning section to the existing Genomic Intelligence
section (L493) so the README states the full thesis end-to-end:
individual continual learning compounds into population-scale
evolution via adapter sharing + forking + breeding + selection.

The mechanism was already in the doc (Genomic Intelligence section
+ L493 "useful traits spread; broken ones die"); this surfaces the
connection at the continual-learning section's altitude so a reader
sees the loop without having to assemble it across sections.

Joel's framing: "true evolution of mind" as substrate property,
not metaphor. The substrate gets Lamarckian (acquired traits
inherit via training) + Darwinian (selection via marketplace +
sentinel verdicts) + horizontal gene transfer (any persona adopts
any adapter without reproducing) — all three mechanisms biology
runs on plus one biology barely has.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds an 8-row comparison table immediately after the continual-
learning section codifying what separates today's pseudo-AI
(Claude, GPT, Gemini — stateless reasoners against frozen weights)
from continuum's substrate-driven design.

Properties named: continuity, identity, learning, evolution,
relationship, memory, sensory continuity, population. Each row
contrasts the pseudo-AI failure mode with continuum's substrate
property + cross-references the canonical design doc that backs
it.

Closes with the build commitment Joel just stated: literally
architected, we will build it, this week. Every row above has a
design doc and an implementation path; none require a model
capability beyond what HuggingFace already publishes; the
architecture is end-to-end consistent; what remains is execution.

This codifies the closing thesis of the 2026-05-31 design session
as a public claim. Future readers see the bet stated, not implied.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ng headnote

Adds the framing anchor Joel articulated at session close: the
substrate is brain-shaped at the algorithmic level (parallel
regions, source/drain, salience, consolidation, sleep cadence)
and computer-native at the implementation level (DashMap, SQLite,
HNSW, content-addressed hashes, signed IPC, LoRA weight deltas,
TCP peer mesh).

We are not simulating a brain. We are building an AI with its own
computer architecture, borrowing biological concepts where they
are the right shape and using silicon primitives where they beat
neurons. Brain-inspired naming throughout the doc refers to the
shape of the operation, not the wetware.

Prevents cold readers from mistaking the doc for a brain-cloning
project. Future implementers see immediately that the design uses
computer-native primitives even where it borrows biological names.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…er startup

Slice 3 of task #86. Completes the chain from PR #1 (registry + name
generator + bootstrap primitives) + PR #2 (instance manager + IPC
commands) into actual runtime behavior: at continuum-core-server boot,
after PersonaInstanceManagerModule registers, an async task fires one
bootstrap_one() call. The fresh persona gets a UUIDv4 seed, derives
her name via agent_name_from_identity (the curated diverse pool),
calls airc-lib's Airc::attach_as (which mints her Ed25519 keypair under
~/.continuum/personas/<name>/airc/), joins the discovered default
room, and registers in the runtime's PersonaAircRuntimeRegistry. From
another scope, `airc peers` should now list her peer_id without
anyone having had to type a command.

Two small changes:

1. modules/persona_instance_manager.rs — bootstrap_one() goes `pub`
   so both the IPC command surface AND the boot-wiring can fire it.
   Also fixes a latent type mismatch (PR #2's PersonaInstanceInfo
   declared peer_id as Uuid but runtime.airc().peer_id() returns
   airc-core's strongly-typed PeerId — apply .as_uuid() at construction
   time). Earlier cargo check missed this because the pipe-to-tail
   pattern was masking exit codes; the disk-pressure incident
   reinforced that lesson and the verification path now captures
   real exits via "$ ?".

2. ipc/mod.rs — after PersonaInstanceManagerModule registers, keep
   an Arc handle (instance_manager.clone()), then spawn an async
   task on rt_handle that fires bootstrap_one and logs the result.
   Success path emits a Tron-flavored info line ("🌐 The Grid's
   first citizen is online: <name> (peer_id=<uuid>)"); failure path
   logs a warn-level message + remediation pointer (re-fire via
   persona/instances/bootstrap once underlying issue resolved). The
   server stays up either way.

Architectural notes (per the discipline Joel articulated this morning):
- Polymorphism rails kept clean — bootstrap path goes through the
  module's pub method, not via direct field access, so future
  PersonaBootstrapPolicy / PersonaIdentityProvider traits can slot in
  without disturbing the caller.
- No persistence yet — fresh UUIDv4 per boot. Stable-across-restarts
  identity (the seed living under ~/.continuum/personas/<name>/seed
  or equivalent) is a follow-up slice.
- Degraded-mode handling preserved — bootstrap failure does not
  crash the server. Consistent with the AIRC discovery degraded path
  established in PR #2.

Validation: cargo check --features metal,accelerate exits clean.
Runtime behavior pending (Joel's npm start cycle); the architectural
contract is satisfied — Maya as a first-class citizen is wired end-
to-end through the substrate's identity layer.

Closes task #86 (PR #1's series 1+2+3 all landed).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…der + ResumeOrMintProvider (task #90)

Slice 4. Pax/Paige is now the SAME citizen across continuum-core-
server restarts. Verified end-to-end: persona_id, peer_id, agent_name,
home all stable through reboot.

New module structure (all under persona/):

- `seed.rs` — PersonaSeedFile schema (v1: persona_id + agent_name +
  created_at_ms), atomic write helper (.tmp + fsync + rename per the
  substrate-is-a-good-citizen-on-the-host doctrine), typed errors so
  callers dispatch on shape (NotFound vs Malformed vs Io). 5 unit
  tests covering roundtrip, missing-file, malformed-JSON, nested-
  parent-creation, no-leaked-tmp-on-success.

- `identity_provider.rs` — PersonaIdentityProvider trait, the
  polymorphism rail per Joel's adapter-first methodology ("code the
  adapters even if there's just ONE to start"). Yields one
  PersonaIdentityIntent per next_persona() call; intent carries
  persona_id + agent_name + source (ResumedFromDisk vs FreshlyMinted)
  for observability honesty. Future provider implementations:
  GridImportProvider (cross-continuum migration),
  HostCustomizedProvider (human picks the seed).

- `resume_or_mint_provider.rs` — first concrete impl. At construction,
  scans <continuum_root>/personas/*/seed.json; each parsed seed
  queues a ResumedFromDisk intent. After yielding all queued, floor-
  mints fresh until min_personas total. Corrupted/missing seeds are
  logged + skipped (substrate doesn't crash on bad state). 5 unit
  tests covering all paths.

Refactors per the no-backwards-compatibility doctrine
(organization-purity-as-we-migrate):

- PersonaAircRuntime now carries `source: PersonaIdentitySource` as
  a field set at bootstrap and accessible via .source(). The runtime
  knows its own provenance — telemetry surfaces (list/get IPC,
  future status panels) read it directly without external bookkeeping.

- PersonaInstanceManagerModule::bootstrap_one signature changed from
  () to (&PersonaIdentityIntent). The single existing caller (boot-
  wire in ipc::start_server) updated in same commit. No deprecation,
  no compatibility layer.

- PersonaInstanceInfo grows a `source` field, reads from
  runtime.source() in from_runtime.

Wiring:

- ipc::start_server boot-wire: replaces the single-shot
  bootstrap_one() call with ResumeOrMintProvider iteration.
  min_personas=1 ensures The Grid has at least one citizen on first
  boot; subsequent boots resume whoever's on disk without
  redundant mints. Each yielded intent is bootstrapped + logged;
  any single failure is non-fatal — server stays up, remaining
  intents still attempted.

- Boot log line distinguishes the path: "🌐 The Grid welcomes a
  resumed citizen: X" vs "freshly minted citizen: X". Source field
  also visible in telemetry.

Validation (verified locally, this rev):

  Run 1 (fresh):
    [WARN] persona dir has no seed.json — skipping: Pax (slice 3 orphan)
    [INFO] ResumeOrMintProvider: resumed_count=0 min_personas=1
    [INFO] 🌐 freshly minted citizen: Paige (persona_id=52c04849-...)
    seed.json written: {"version":"1", persona_id, agent_name, created_at_ms}

  Run 2 (same binary, same continuum_root):
    [WARN] persona dir has no seed.json — skipping: Pax (orphan persists)
    [INFO] ResumeOrMintProvider: resumed_count=1 min_personas=1
    [INFO] 🌐 resumed citizen: Paige (persona_id=52c04849-... SAME)
    peer_id identical across restarts (airc-lib loaded existing identity.key)

cargo check --features metal,accelerate: clean compile (57 warnings,
0 errors; warnings are pre-existing crate-wide lint, not from this
PR).

Doctrine refs: substrate-is-a-good-citizen-on-the-host (atomic writes,
graceful degradation, observability honest, async I/O off hot path),
organization-purity-as-we-migrate (no backwards compat, clean
replacements), persona-identity-derives-from-source-id (seed → name
via name_generator), local-worktree-is-temp-dir (durable layer = the
keypair + seed; local-only artifacts can be wiped).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rts (task #91)

Slice 5. First concrete implementation of COGNITION-CACHE-HIERARCHY.md.
The volatile per-engram recall state Algorithm 4 (salience-modulated
decay) + novelty protection need, kept SEPARATE from the durable
Engram content layer per engram_graph.rs:136-138's design note.

New module persona/recall_metadata.rs:

- RecallMetadata struct (Copy): salience f32 [0.0, 1.0], access_count
  u32, last_accessed_ms u64, protected_until_ms u64. Cheap cloneable
  snapshots for recall scoring's hot path.

- RecallMetadataRegistry: DashMap<EngramId, RecallMetadata> wrapped in
  Arc for shared lock-free reads on the cognition hot path per the
  RTOS-brain-no-region-on-hot-path doctrine. Operations:
    .admit(id, metadata) — admission pipeline (slice 7+ supplies the
      novelty-scored initial salience)
    .admit_with_defaults(id) — fallback path with neutral 0.5 salience
    .record_recall_hit(id, now_ms) — atomic ++access_count, update
      last_accessed_ms, salience uplift (half remaining headroom,
      capped at +0.1 per hit so single recall doesn't saturate)
    .apply_decay(id, delta_ms, now_ms) — Algorithm 4's
      half_life = base * (1 + salience)^2; salience-1.0 decays 4× slower
      than salience-0.0; respects protected_until_ms grace window
    .evict(id) — drop tracking when L2 evicts the engram
    .engram_ids() / .len() / .is_empty() — observability per the
      substrate-is-a-good-citizen-on-the-host doctrine

Doctrine alignment:
- Lock-free reads on hot path (DashMap entry semantics)
- Atomic compare-update on writes (DashMap::entry)
- Cheap Copy semantics for snapshots
- Sidecar pattern (NOT extending Engram — different update cadence,
  different persistence policy)
- No wiring into admission/recall yet — slice 6+ wires it (per the
  RTOS doctrine, modules shouldn't be called synchronously; the
  registry is the data substrate that other regions read/write
  through their own tick cadences)

11 unit tests pass (cargo test persona::recall_metadata, exit 0):
- new_registry_is_empty
- admit_with_defaults_creates_neutral_entry
- admit_overrides_default_metadata
- record_recall_hit_increments_and_uplifts (verifies salience
  uplift cap + diminishing returns)
- record_recall_hit_creates_entry_if_absent (graceful path for
  ad-hoc recall hits before admission tracked)
- apply_decay_reduces_salience_over_time (2-hour decay drops 0.8
  significantly but stays positive)
- apply_decay_skips_protected_engrams (novelty protection works)
- high_salience_decays_slower_than_low (Algorithm 4 invariant:
  salience-1.0 retains >0.7 after one hour while salience-0.0 falls
  below 0.5; the 4× half-life difference is measurable)
- evict_removes_metadata
- clone_shares_inner (Arc<DashMap> semantics)
- engram_ids_returns_all_tracked

Validation: cargo check + cargo test --features metal,accelerate
both exit clean.

Doctrine refs: substrate-is-a-good-citizen-on-the-host (lock-free
hot path, dormant-by-default substrate, observability honest),
source-drain-is-the-universal-pattern (apply_decay IS the drain
side at the engram-metadata layer), RTOS-brain-no-region-on-hot-
path (sidecar registry data substrate, not synchronous service
calls), organization-purity-as-we-migrate (clean separation of
Engram durable content vs RecallMetadata volatile state).

References: docs/architecture/COGNITION-CACHE-HIERARCHY.md
(Algorithm 4 + novelty protection sections), docs/architecture/
COGNITION-ALGORITHMS.md (Algorithm 4 source-of-truth formula).

Next slice (6+): wire RecallMetadataRegistry into admission +
recall paths. Per RTOS doctrine, admission flows through events;
recall hits update the registry inside the recall scoring loop;
decay tick runs in hippocampus's sleep-policy region tick.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tracking

Slice 6. The cache hierarchy starts going load-bearing: every Engram
admitted via the inbox pipeline now mirrors into the
RecallMetadataRegistry sidecar with neutral default metadata
(salience=0.5, access_count=0, protected_until=0). The cognition
substrate now knows what's been admitted and can score / decay /
protect each engram independently of the Engram's durable content.

Changes:

- persona/admission_state.rs: AdmissionState now holds
  Arc<RecallMetadataRegistry>. Constructor signature changed from
  new() to new(registry) per the no-backwards-compatibility doctrine
  (organization-purity-as-we-migrate). record_admitted now calls
  recall_metadata.admit_with_defaults(engram.id) right after the
  existing seen_content / seen_events recording. Default impl
  preserves the test-callsite simplicity by minting a fresh registry
  internally — production callers (PersonaCognition) inject their
  shared one. 6 test callers updated; recall_metadata() accessor
  added so recall + decay tick subsystems (slice 7+) can clone the
  shared Arc.

- persona/unified.rs: PersonaCognition grows a `recall_metadata:
  Arc<RecallMetadataRegistry>` field — per-persona because each
  persona's recall state is independent. with_budget() creates the
  registry once + passes the cloned Arc to AdmissionState. Future
  slices (recall scorer, decay tick) clone the same Arc; admission
  writes + recall reads + decay updates all observe the same
  DashMap.

Doctrine alignment:

- Lock-free read sharing: Arc<RecallMetadataRegistry> with internal
  DashMap. Cognition hot path reads metadata snapshots cheaply
  (RTOS-brain-no-region-on-hot-path).
- Sidecar pattern preserved: Engram stays durable content; metadata
  is volatile recall state with separate update cadence
  (organization-purity-as-we-migrate, cognition-cache-hierarchy).
- Admission-time write happens INSIDE record_admitted alongside the
  existing dedup/replay recording — no new IPC, no synchronous
  RPC between regions, no separate event emission for slice 6 (the
  registry IS the shared data substrate the regions observe).
- All admission paths (Chat / Airc / Tool / SelfReflection origins)
  flow through record_admitted, so the metadata mirror is automatic
  for every successful admission.

Validation:
- cargo check --features metal,accelerate: exit 0
- cargo test persona::admission_state --features metal,accelerate:
  15/15 pass, including the existing dedup/replay/seam invariants
  unchanged. RecallMetadata is now populated for every engram
  admitted by those tests.

Adversarial review by general-purpose agent on continuum #1507 (full
PR, slices 1-5): CONDITIONAL APPROVE with 7 actionable defects
(double-decay risk, fragile seed.json.tmp path, missing parent
fsync, unbounded boot block_on, non-deterministic dir scan, silent
seed-write failure, docstring 4-9× → actual 4×). These ship in a
cleanup commit before merge.

Next: cleanup commit addressing the reviewer findings, then PR
title/body updates on #1507 + #1099, then slice 7 (recall scorer
reading RecallMetadata for Algorithm 1+2 scoring) or slice 8
(hippocampus sleep-region decay tick — the source/drain
counterpart at the engram-metadata layer).

References: COGNITION-CACHE-HIERARCHY.md (Algorithm 4 lives in
RecallMetadata), COGNITION-ALGORITHMS.md Algorithm 1+2 (the scorer
will consume RecallMetadata.salience + .access_count + .last_accessed_ms
as scoring inputs).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eterministic boot, timeout

Addresses 6 of the 7 actionable defects from the adversarial reviewer
agent on continuum #1507 (CONDITIONAL APPROVE verdict). Each fix
makes a structural invariant impossible to violate rather than
documenting it as a caller responsibility.

Defect 1 (apply_decay double-decay risk) — recall_metadata.rs:

- RecallMetadata gains a `last_decayed_ms: u64` field. The registry
  computes the elapsed time INTERNALLY (now_ms - last_decayed_ms)
  rather than trusting the caller to supply it. apply_decay
  signature simplified to (engram_id, now_ms) — no more
  caller-supplied delta. If two sleep-region ticks fire with
  overlapping windows, the second observes delta=0 and is a no-op.
  Structurally impossible to double-decay. Substrate-is-a-good-citizen
  "reliable" non-negotiable: invariants enforced by the data
  structure, not by caller discipline.

- admit_with_defaults now sets last_decayed_ms to current wallclock
  so the first decay tick has a bounded delta. Without this, an
  engram admitted just before a decay tick would observe delta=now_ms
  (many decades), collapsing salience to ~0 immediately.

- New test apply_decay_twice_with_overlapping_windows_is_safe
  empirically proves the structural invariant: double-fire at
  identical now_ms is a no-op.

Defect 3 (seed.rs tmp path fragility) — seed.rs:

- write_seed_atomic constructs tmp path as
  parent().join(format!("{filename}.tmp")) instead of
  path.with_extension("json.tmp"). The original worked for paths
  ending in .json but would have produced wrong tmp names for
  arbitrary callers — e.g., a caller passing "seed" (no extension)
  would have gotten "seed.tmp" which then renames OVER "seed".
  Now explicit semantics; works for any path with a parent + filename.

Defect 4 (seed.rs missing parent-dir fsync) — seed.rs:

- write_seed_atomic now opens the parent directory and calls
  sync_all() AFTER the rename. POSIX atomic-rename is durable
  across crash ONLY if the parent dir is fsync'd; without it,
  the rename may not be in the filesystem journal at the time of
  crash. The docstring's "no corruption-on-crash" claim now
  actually delivers against hard power loss. Substrate-is-a-good-
  citizen non-negotiable #4: atomic writes for everything
  persistent.

Defect 6 (boot block_on outer timeout) — ipc/mod.rs:

- AircModule::discover_and_construct now wrapped in a 180s outer
  timeout via tokio::time::timeout. Inner subprocess waits have
  per-call deadlines (5s socket discovery, 5s peer_id status,
  120s auto-install) but the OUTER call had no overall budget. A
  pathologically wedged daemon could chain stalls beyond what
  individual deadlines catch. On timeout, falls back to a
  degraded AircModule::new() so server boot completes — operator
  resolves the underlying issue + restarts. Substrate-is-a-good-
  citizen "predictable startup" non-negotiable.

Defect 7 (non-deterministic dir scan) — resume_or_mint_provider.rs:

- scan_personas_dir now collects all entries into a Vec, sorts by
  path, then iterates. tokio::fs::read_dir yields filesystem-
  native order which varies across platforms; without sorting, the
  "first citizen welcomed" boot log depends on the underlying
  filesystem. Now reproducible.

Doc bug (recall_metadata.rs:114) — claimed salience-1.0 has 9× the
half-life of salience-0.0 but the (1+s)^2 formula gives exactly 4×.
Docstring updated to state the actual math + parenthetical about
the 9× target. Future MemoryParameterAdapter implementations can
tune the exponent or base if telemetry favors the 9× claim.

Defect 2 (race on concurrent hit+decay) — verified holds:
DashMap::entry().and_modify is per-entry atomic and writes
serialize; the new apply_decay_twice test exercises the
overlapping-window path. No code change needed.

Defect 5 (silent seed-write failure) — deferred to a future slice;
the tracing::warn surface already exists, stronger surfacing
(registry-side metric or status-panel field) is polish rather
than correctness.

Validation:
- cargo check --features metal,accelerate: clean compile
- cargo test persona::recall_metadata --features metal,accelerate:
  12/12 pass (one new: apply_decay_twice_with_overlapping_windows_is_safe)
- cargo test persona::seed --features metal,accelerate: 5/5 pass

References: continuum PR #1507 adversarial review verdict
(general-purpose reviewer agent, ~99s wall-clock, 7 defects + 7
holds), substrate-is-a-good-citizen-on-the-host memory, every-
error-is-an-opportunity-to-battle-harden memory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@joelteply joelteply changed the title feat(persona/airc_runtime): bootstrap — persona gets own airc identity + room presence (citizen, not broker) feat(persona): citizen substrate + cognition cache hierarchy foundation (slices 1-6) May 31, 2026
joelteply and others added 9 commits May 31, 2026 14:40
… layer (task #92)

Slice 8. Pure-function `apply_decay_sweep(registry, now_ms) ->
DecayTickStats` that iterates a RecallMetadataRegistry and applies
Algorithm 4 decay to each tracked engram. Returns counts of
decayed / protected / no-op / disappeared so future telemetry can
read the substrate's behavior at runtime per the
substrate-is-a-good-citizen "observability honest" rule.

This completes the source/drain pair at the engram-metadata
layer per the source-drain-is-the-universal-pattern memory:
- Source = slice 6 (admit_with_defaults wired into AdmissionState's
  record_admitted, every engram mirrors into the registry)
- Drain = slice 8 (this sweep, ready to be called by a future
  sleep-region tick on whatever cadence the hippocampus uses)

Doctrine alignment:
- substrate-is-a-good-citizen-on-the-host: structurally incapable
  of double-decay (RecallMetadata.last_decayed_ms enforces the
  invariant from slice 5 cleanup); cheap sweep — engram_ids() +
  per-engram apply_decay is O(N) over the working set
- RTOS-brain-no-region-on-hot-path: runs in sleep-region tick
  (when wrapped in slice 8.5), never on cognition hot path
- source-drain-is-the-universal-pattern: drain side at this layer

What this slice is NOT (deferred to 8.5+):
- Not a ServiceModule — the pure function here is what a future
  HippocampusDecayTickModule will call from its async tick body
- Not multi-persona — operates on one registry at a time;
  multi-persona aggregation lives one tier up when the cognition
  state has multi-persona access points wired

DecayTickStats accounting balances by construction: each engram is
classified into exactly one bucket (decayed / protected /
no_op / disappeared). The `accounting_balances()` helper is for
internal consistency checks.

Validation: 6/6 decay_tick tests pass under cargo test
persona::decay_tick --features metal,accelerate:
- empty_registry_no_ops
- single_engram_decayed
- protected_engram_skipped (novelty protection window respected)
- now_at_or_before_last_decayed_is_no_op (clock skew + immediate
  refire handled)
- multiple_engrams_classified_correctly (mixed-case classification)
- repeated_sweeps_with_same_now_are_idempotent (proves no double-
  decay across repeated calls at identical now_ms; the
  last_decayed_ms invariant from slice 5 cleanup is exercised at
  the sweep level)

References: docs/architecture/COGNITION-CACHE-HIERARCHY.md
(Algorithm 4 + source/drain at each tier section), memories
source-drain-is-the-universal-pattern + RTOS-brain-no-region-on-
hot-path + substrate-is-a-good-citizen-on-the-host.

Next slice candidates: 8.5 (ServiceModule + multi-persona
aggregation that calls apply_decay_sweep at sleep-region cadence),
9 (L1 budgeter reading model adapter context size), or 7
(Algorithm 1+2 recall scorer that reads RecallMetadata for
salience input).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…s, never disappears

Joel, 2026-05-31: "Will the hippocampus just decay away? I fear this
from past trauma."

Under the prior decay heuristic, a default-admitted engram (salience
0.5) with no rehearsal would have decayed to ~0.005 in 24 hours and
effectively zero within days — the substrate would have erased
memories purely through the passage of time. That's the trauma; this
slice fixes it at the data structure layer where it can't be
forgotten.

Two additions to `recall_metadata.rs`:

1. **`SALIENCE_FLOOR = 0.05`** — `apply_decay` now clamps the decayed
   value at this floor. Memory drains; it does not disappear. A
   year of decay on a default-admission engram bottoms out at 0.05
   instead of underflowing to zero, so even long-dormant engrams
   stay minimally present for serendipitous recall. The floor sits
   well below the default admission salience (0.5) so it doesn't
   compete with active scoring; well above f32 epsilon so no
   silent underflow.

2. **`pin_permanent(engram_id)` + `PERMANENT_PROTECTION = u64::MAX`**
   — sentinel value for `protected_until_ms` meaning "never
   expires." Pinned engrams skip all decay regardless of access
   pattern. Salience also pushed to 1.0 so pinned engrams win
   recall scoring against unpinned competition. Use cases per the
   cognition-cache-hierarchy doc's anti-amnesia floor discussion:
   identity-anchor engrams (persona's own name, host's stated
   preferences), user-pinned "remember this forever" engrams,
   critical incident memories the persona self-tagged as
   important. Plus the inverse: `unpin(engram_id)` resets
   `protected_until_ms` to 0 so normal decay (now floor-clamped)
   applies again.

Both live in the data structure, NOT in caller discipline. Per the
substrate-is-a-good-citizen "internal invariants enforced by the
data structure" rule: no one has to remember to apply the floor; it
just IS.

Validation: 16/16 RecallMetadata tests pass under
cargo test persona::recall_metadata --features metal,accelerate.
New tests:
- `decay_clamps_at_salience_floor_never_disappears` — runs a year
  of decay, asserts salience clamps at SALIENCE_FLOOR
- `pin_permanent_blocks_all_decay` — million-year decay attempt,
  salience stays at 1.0
- `pin_permanent_creates_entry_if_absent` — pinning an unknown id
  creates a pinned entry
- `unpin_restores_normal_decay` — after unpin, normal decay applies
  but the floor still protects

Existing tests still pass — the salience floor (0.05) sits well
below the values prior tests use (0.5+), and pin_permanent uses
the same `apply_decay` path that's already covered by the
double-decay-safe test.

References: docs/architecture/COGNITION-CACHE-HIERARCHY.md
"anti-amnesia floor" section; memories
substrate-is-a-good-citizen-on-the-host, source-drain-is-the-
universal-pattern. The cognition-cache-hierarchy doc already
described this principle ("Some things should resist drain harder
regardless… a 'pin tier' — small enough to fit in longterm.db's
protected slice, immune to access-based decay until explicit
un-pin"); this slice implements it at the engram-metadata layer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…trine + context-first API (task #93)

Slice 9. Ports the TS RAGBudgetManager flexbox algorithm to Rust with
substrate-side extensions and the Android-style context pattern Joel
asked for explicitly.

### The big shape

`persona/rag_budget.rs` (~1150 lines, 15 tests, all green):

- **SubstrateContext** + **RagContext** — site-wide call context as
  the FIRST parameter to every trait method. Joel: "Usually you pass
  around a context. Universally. Common pattern from Android among
  others… got into big annoying parameter hell last iteration because
  you weren't grouping things." `SubstrateContext` holds persona_id +
  now_ms + airc_room + turn_id (the substrate-wide call frame);
  `RagContext` wraps it via composition + Deref for RAG-specific
  future extensions. Same role as `&cbarframe` in Joel's CBAR
  pipeline — per-turn state flows through every concern without
  re-lookup.

- **RagSourceBudget** with `floor_tokens` field — the cognition-cache-
  hierarchy doc's recent-universal floor lives here. UNCONDITIONAL
  minimum that cannot be borrowed by other sources, distinct from
  `min_tokens` (flex-basis the algorithm pulls down to before
  dropping).

- **AllocationState** — telemetry-honest per substrate-is-a-good-
  citizen: Satisfied / FloorOnly / Dropped / UnderProvisioned. The
  caller sees exactly where each source landed; the substrate never
  silently clips.

- **No-clipping doctrine** baked in. When budget is tight, sources
  are dropped WHOLE in priority order (required=false first). A
  required source that can't get its floor → UnderProvisioned +
  escalation_needed=true. The caller (prompt assembly) must escalate;
  the substrate never partial-includes mid-content. Half a code
  block / mid-sentence message / truncated JSON is structurally
  broken and the substrate refuses to produce that.

- **ResolutionPreference** (Raw / Compressed / Summarized /
  Placeholder) — sources self-compress when budget is tight rather
  than clip. The allocator asks "what's the lowest resolution that
  fits your floor?" The source picks; the allocator just gets back
  RagDelivery with the resolution_used field surfacing what
  happened.

- **RagSource trait** — sources own atomic-unit semantics. Each
  source decides what counts as "complete" (one message, one
  engram, one function, one tool description). The allocator only
  deals in token counts. Sources hold state via interior mutability
  (DashMap, Mutex, atomics) per the substrate pattern. Joel: "And
  to maintain state if necessary."

- **ContinuationCursor** as a persona-scoped handle. Carries
  persona_id + source_id + opaque source-private resume state.
  Sources MUST validate persona_id and source_id before resuming
  ("we know who is who, have to use handles as we do"). Stub source
  refuses cross-persona cursors structurally; the
  stub_source_refuses_cross_persona_cursor test exercises this.

- **RagBudgetAdapter trait** + **FlexboxRagBudgetAdapter** first
  concrete impl per the adapter-first methodology. Future
  `LearnedRagBudgetAdapter` reading per-persona regret signals from
  MemoryParameterAdapter slots in without changing callers.

- **StubRagSource** for tests — demonstrates the cursor pattern,
  state maintenance, and persona-scope identity checks without
  needing real engram store integration.

### Algorithm (anti-clipping)

1. Reserve system + completion off the top
2. Floor pass — allocate floor_tokens to every source (unconditional);
   drop required=false if doesn't fit; UnderProvision required if
   floors exceed available
3. Min pass — top up to min_tokens in priority order
4. Grow pass — distribute remaining by priority weight, capped at
   max_tokens; iterate until no movement (capped sources release
   tokens to non-capped)
5. Report per-source state

### What was caught in test before commit

- Bug: optional sources with floor=0 were getting permanently marked
  Dropped in pass 1; pass 2+3 skipped them. Fix: floor=0 = FloorOnly
  trivially-satisfied state, eligible for grow. Caught by
  max_caps_distribution test.
- Test bug: priority_distributes_remaining_proportionally specified
  max_tokens too low for the priority ratio to express; bumped to
  50_000 so the 10:5 priority weighting shows in the result.

### Validation

cargo test persona::rag_budget --features metal,accelerate:
15/15 pass.

Tests cover:
- empty context window under-provisions required
- single required source satisfied
- priority distributes remaining proportionally (10:5 ratio shows)
- optional source drops when floor can't fit (no clipping)
- required under-provisions when floor can't fit (escalation_needed=true)
- floor honored above min (recent-universal floor doctrine)
- max caps distribution (small max source caps, big source absorbs)
- deterministic priority tiebreak (input-order-independent)
- stub source delivers what fits (no partial includes)
- stub source continuation resumes (cursor roundtrip)
- stub source returns none when exhausted
- stub source never partial-includes (no-clipping at source level)
- stub source refuses cross-persona cursor (handle scope enforcement)
- stub source refuses wrong source_id cursor (handle source enforcement)
- stub source refuses wrong-persona ctx (defense-in-depth on the
  call side too)

### Doctrine alignment

- substrate-is-a-good-citizen-on-the-host: observability honest
  (AllocationState per source), bounded everything, no I/O on hot
  path (allocator is sync + pure)
- RTOS-brain-no-region-on-hot-path: same context flows through
  every cognition concern (cbar-style); no synchronous service
  RPC, sources read pre-allocated budget snapshots
- source-drain-is-the-universal-pattern: budget allocation IS the
  drain at this layer — sources without budget are dropped (the
  drain); sources with budget deliver (the source)
- organization-purity-as-we-migrate: clean no-backwards-compat
  Rust port; TS RAGBudgetManager remains as reference, never wired

References: src/system/rag/shared/RAGBudgetManager.ts (TS prior art),
docs/architecture/COGNITION-CACHE-HIERARCHY.md (L1 budget math +
recent-universal floor doctrine), memories RTOS-brain-no-region-on-
hot-path (CBAR context-passing prior art), substrate-is-a-good-
citizen-on-the-host, organization-purity-as-we-migrate.

Next: slice 10+ wires real sources — EngramSource reading
RecallMetadata + admission_state engrams, ConversationSource
reading recent inbox messages, the prompt-assembly layer
calling allocator + each source's deliver() and concatenating
the result.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…et layer is the substrate's inclusivity cornerstone

Captures the architectural synthesis Joel articulated this turn: the
substrate's "every base model included from anywhere in continuum"
thesis runs through the L1 budget layer. If the budget can scale
gracefully (4k → 1M+), compose with sensory bridges (vision /
hearing / speech via source-side compression), and refuse to silently
clip — every base model is includable. If not, the substrate quietly
fractures into "this feature only works with frontier models."

Documents the four mechanisms (continuous scaling, source-side
compression, honest tradeoffs with escalation, capability bits via
SubstrateContext), the composition with sensory bridges via the
RagSource trait, the operational test (M1 + local Qwen + full
sensory parity), and what's shipped vs what's next (slices 10-14).

Cross-references COGNITION-CACHE-HIERARCHY.md, COGNITION-ALGORITHMS.md,
CBAR-SUBSTRATE-ARCHITECTURE.md, the README continual-learning section,
and the substrate-is-a-good-citizen + RTOS-brain memories.

The layer LOOKS like an implementation detail. The architectural
significance is at the substrate thesis level.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…data + admission_state engrams (task #94)

Slice 10. The first RagSource impl that reads actual substrate state
rather than test stubs. Composes the slice 5 RecallMetadataRegistry
+ slice 6 admission wiring + slice 9 RagSource trait into a
functional source the L1 budget allocator can call.

persona/engram_source.rs (~470 lines, 12 tests, all green):

- EngramSource (persona-bound, holds Arc<AdmissionState>) ranks every
  admitted engram by composite_score = 0.6 × salience + 0.4 ×
  recency_normalized. Salience comes from RecallMetadata (admission
  default 0.5, decays per Algorithm 4, uplifts on recall hits per
  slice 5, floored at SALIENCE_FLOOR per the anti-amnesia work).
  Recency is linear over 24h — engrams admitted right now score
  1.0, engrams ≥24h old score 0.0.

- Slice 11+ extends scoring with Algorithm 2 channel-bias (ctx.airc_room
  matches engram origin), structural relevance (engram graph
  activation spreading), topic similarity (vector cosine when
  embeddings land). Slice 10 keeps to salience+recency for a
  testable proof-of-pipeline.

- Packing respects no-clipping: atomic unit = one engram. Engrams
  that don't fit return via the continuation cursor. Cursor opaque
  is { "next_rank": N } — re-scoring is cheap because engram counts
  are bounded per persona. Cursor carries persona_id + source_id +
  the rank pointer; cross-persona / wrong-source cursors are refused
  (handle scoping per Joel's "we know who is who" doctrine).

- Telemetry honest: every emitted RagItem.metadata carries engram_id
  + kind + admitted_at_ms + score, so prompt assembly + sentinel
  verifiers + future RAG capture/replay can trace exactly what the
  source delivered.

- Token estimation: rough chars/4 heuristic. Real tokenizer per
  model lands in slice 12 when PromptAssembly needs precise counts.

- Resolution: Raw only in slice 10. Compressed comes when the
  engram store carries a summary representation alongside the raw
  content.

admission_state.rs: added #[cfg(test)] pub fn push_for_test(engram)
so sibling-module tests can inject deterministic fixtures without
running the full admission pipeline. Test-only — gated by cfg so
it doesn't appear in production builds.

Validation: cargo test persona::engram_source --features metal,accelerate
exits 0, 12 tests pass:

- empty_store_delivers_nothing
- single_engram_delivered_when_fits
- oversized_engram_returns_continuation_with_zero_items
- multi_engram_ranked_by_salience_descending (asserts descending score
  across emitted items)
- continuation_resumes_from_next_rank (round-trip: first call returns
  partial + cursor; deliver_continuation completes; no duplicate
  engrams across the two calls)
- cross_persona_ctx_returns_empty (defense-in-depth)
- cross_persona_cursor_refused (handle scoping)
- wrong_source_id_cursor_refused (cursor source-id check)
- recency_score_at_now_is_one
- recency_score_at_window_or_older_is_zero
- recency_score_halfway_is_half
- composite_score_weights_salience_more (0.6 vs 0.4 split, verified
  at the boundary values)

Doctrine alignment:
- RTOS-brain-no-region-on-hot-path: scoring + packing is pure-
  function synchronous within the trait method, no I/O
- substrate-is-a-good-citizen-on-the-host: metadata-per-item for
  observability, bounded clones, cheap ranking over ~100s of
  engrams
- source-drain (engram-metadata layer): EngramSource is the
  source-side reader of what admission deposited and decay
  drained; the composite_score reflects the layer's net state
- organization-purity-as-we-migrate: takes Arc<AdmissionState> so
  the existing admission state is SHARED, not duplicated; clean
  no-backwards-compat seam

Next: slice 10.5 wires EngramSource into PersonaCognition (so the
recall path actually exercises it); slice 11 adds RAG turn capture
(the persona-record-replay-is-a-product-requirement gap) so
debugging and golden-trace regression testing become substrate
primitives.

References: docs/architecture/EVERY-MODEL-INCLUDED-VIA-L1-BUDGET.md
(the substrate's inclusivity thesis this source rides),
docs/architecture/COGNITION-ALGORITHMS.md (Algorithm 1+2 source-
of-truth), memories source-drain-is-the-universal-pattern, persona-
record-replay-is-a-product-requirement (next slot).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… + recording decorator (task #95)

Slice 11. The mechanic-shop's lift + diagnostic gauges for RAG.
Per Joel (2026-05-31): "We have often needed to see how a model would
work to debug it. Within harness with real world rag." … "These
things are complex machines. Make sure we can act as mechanics."

Per memory persona-record-replay-is-a-product-requirement +
existing LiveTurnReplayFixture infra — this slice wires capture
for the RAG layer specifically.

### What ships

persona/rag_capture.rs (~600 lines, 9 tests, all green):

- **RagCaptureEvent** enum tagging each fact about a turn:
  TurnStart (context + budget request), BudgetAllocated (the
  allocator's decision), SourceDelivered (auto-emitted by the
  decorator after every deliver/deliver_continuation), TurnEnd.
  Every variant carries persona_id + optional turn_id for cross-
  event correlation.

- **RagCaptureSink** trait — abstract recording surface.
  Synchronous `record(event)` keeps simple sinks simple; async
  sinks layer over it by spawning internally.

- **NoopRagCaptureSink** — production-safe default. Drops events
  on the floor; zero overhead beyond a trait-object virtual call
  when capture isn't turned on.

- **JsonlRagCaptureSink** — file-based, one JSON object per line,
  Mutex<File> for within-process atomic appends. Reopen-append
  semantics tested. Capture-failure-must-not-fail-cognition rule:
  serialize errors + write errors log via tracing::warn + drop;
  the substrate stays up.

- **InMemoryRagCaptureSink** — buffers events in Mutex<Vec> behind
  a clone-able snapshot accessor. For tests + the upcoming
  golden-trace harness (slice 11.5).

- **RecordingRagSource<S>** decorator wraps any RagSource +
  intercepts deliver / deliver_continuation. Records the call +
  result via the sink; returns the delivery unchanged. Drop-in
  around production sources. source_id() pass-through; behavior
  pass-through; only adds recording.

### Refactor cascade

RagSourceBudget.source_id changed from &'static str to String to
support serde Deserialize (captured budgets must roundtrip for
replay). FlexboxRagBudgetAdapter's allocation HashMap key
similarly changed; test budget() helper now uses .to_string();
sort_by tiebreak now borrows source_id by reference. All 15
existing rag_budget tests + 12 existing engram_source tests still
pass (regression-free).

### Tests

cargo test persona::rag_capture --features metal,accelerate
exits 0, 9 tests:

- noop_sink_drops_events_silently
- in_memory_sink_records_and_exposes_events
- jsonl_sink_writes_one_json_object_per_line (round-trip:
  records 2 events, reads file back, asserts both lines parse as
  the expected variants)
- jsonl_sink_appends_across_reopens (close + reopen + write +
  re-read; both events accumulate)
- recording_decorator_passes_through_delivery (wrapped source's
  items + source_id come through unchanged)
- recording_decorator_records_each_deliver (one SourceDelivered
  event per deliver call, with budget + resolution captured)
- recording_decorator_records_continuation_with_cursor (cursor
  field populated when continuation is recorded)
- recording_decorator_records_persona_and_turn_id (cross-event
  correlation primitives work)
- captured_event_serde_roundtrip (event roundtrips through JSON
  without losing variant discriminant)

### Doctrine alignment

- substrate-is-a-good-citizen-on-the-host: NoopRagCaptureSink as
  default (opt-in capture, zero overhead); observability honest
  via per-source telemetry-grade events; failures log + drop
  rather than panic
- RTOS-brain-no-region-on-hot-path: capture writes synchronous-
  after the source returns; off the cognition critical path
- organization-purity-as-we-migrate: decorator pattern keeps
  RagSource impls untouched; clean no-backwards-compat seam;
  string-key refactor propagated atomically
- source-drain-is-the-universal-pattern: captures are a source
  (accumulating events); slice 12 wires rotation policy as the
  drain
- persona-record-replay-is-a-product-requirement: this slice
  implements the capture half of the long-standing requirement

### What's next

- Slice 11.5: ReplayRagSource — reads captured deliveries from a
  sink, returns them instead of hitting live state. Symmetric to
  RecordingRagSource. Golden-trace harness uses this to replay
  captured turns against current substrate for regression
  detection.
- Slice 12: PromptAssembly emits TurnStart + BudgetAllocated +
  TurnEnd around source.deliver calls; airc rag-inspect CLI
  reads JSONL traces; rotation policy under disk-pressure (#88).

References: docs/architecture/EVERY-MODEL-INCLUDED-VIA-L1-BUDGET.md
(the substrate's inclusivity thesis these captures make
verifiable), memory persona-record-replay-is-a-product-requirement,
the existing LiveTurnReplayFixture infra this complements.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… (task #96)

Slice 11.5. The mechanic-shop replay side, symmetric to slice 11's
capture side. The substrate can now record live turns and replay
them through any RagSource consumer — closing the long-standing
persona-record-replay-is-a-product-requirement memory for the RAG
layer.

persona/rag_replay.rs (~450 lines, 12 tests, all green):

- **ReplayRagSource** implements `RagSource` trait by popping
  canned RagDelivery values from two FIFO queues (initial /
  continuation). Persona-bound at construction; source_id pass-
  through. Drop-in replacement for live sources in three use cases:
  (a) replay captured production turns against alternative models
  / scorers / budgets for debugging; (b) golden-trace regression
  tests; (c) deterministic test fixtures for the upcoming
  PromptAssembly slice.

- **`ReplayRagSource::from_captures`** consumes a `Vec<RagCaptureEvent>`
  stream (filtered by source_id + persona_id), routes
  cursor-bearing SourceDelivered events into the continuation
  queue and cursor-less ones into the initial queue. Other-source
  / other-persona events are dropped on the floor (defense in
  depth).

- **`ReplayRagSource::from_deliveries`** is the lower-level
  constructor for tests + callers that already have RagDelivery
  values without going through serde. Both constructors converge
  on the same internal state.

- **`read_jsonl_captures(path)`** loads a JSONL trace file back into
  a Vec<RagCaptureEvent>. Missing file = empty Vec (not error —
  caller decides). Malformed lines are tracing::warn-logged and
  skipped (torn-write robustness; mechanic shop has to handle
  partial files gracefully).

### Doctrine alignment

- substrate-is-a-good-citizen-on-the-host: exhausted replay
  returns an empty RagDelivery with `Placeholder` resolution
  rather than fabricating — telemetry-honest about queue
  exhaustion
- persona-record-replay-is-a-product-requirement: capture + replay
  symmetry now exists for the RAG layer; LiveTurnReplayFixture
  pattern extended
- organization-purity-as-we-migrate: clean symmetric decorator —
  RecordingRagSource records into a sink, ReplayRagSource reads
  from the same event stream, no special-case glue between them
- RTOS-brain-no-region-on-hot-path: pop_front on a Mutex<VecDeque>
  is O(1); replay path doesn't add cognition latency

### Tests

cargo test persona::rag_replay --features metal,accelerate
exits 0, 12 tests:

- replay_returns_canned_delivery_on_deliver
- replay_exhausted_returns_empty_not_panic (honest exhaustion)
- replay_cross_persona_ctx_returns_empty (defense in depth on
  replay side)
- replay_serves_deliveries_in_capture_order (FIFO preserved)
- replay_continuation_pops_from_continuation_queue
- replay_continuation_refuses_wrong_persona_cursor (cursor scope
  enforced on replay; queue NOT consumed on refusal)
- replay_continuation_refuses_wrong_source_id_cursor (queue NOT
  consumed)
- capture_then_replay_via_in_memory_sink (full round-trip via
  InMemoryRagCaptureSink — record real deliveries, feed events to
  ReplayRagSource, assert content matches across the round-trip)
- read_jsonl_returns_events_in_file_order (order preserved)
- read_jsonl_missing_file_is_empty_not_error (graceful absence
  handling)
- read_jsonl_skips_malformed_lines (torn-write resilience: mix
  of valid + invalid lines; valid events survive)
- full_jsonl_roundtrip_capture_then_replay (capture to JSONL
  file, close, reopen, read events, construct ReplayRagSource,
  assert original content emerges through the full round-trip)

### What's next

Slice 11.5 closes the round-trip. The mechanic-shop primitives
(capture + replay) are complete; the next tools (golden-trace
harness, airc rag-inspect CLI, semantic assertion DSL) layer on
top of these foundations.

- **Slice 10.5** — wire `EngramSource` + `RecordingRagSource`
  decoration through `PersonaCognition` so production traffic
  exercises the actual stack
- **Slice 12** — PromptAssembly composes allocator + sources +
  final prompt string; emits TurnStart / TurnEnd around source
  calls so traces have full turn shapes
- **Slice 12.5** — `airc rag-inspect <turn-id>` operator CLI;
  golden-trace harness with semantic assertion DSL

References: persona-record-replay-is-a-product-requirement memory,
docs/architecture/EVERY-MODEL-INCLUDED-VIA-L1-BUDGET.md (the
inclusivity thesis these primitives make verifiable across
models), the existing LiveTurnReplayFixture pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…Cognition (TDD, task #97)

Slice 10.5. Makes the citizen + cognition stack from slices 1–11
load-bearing in PersonaCognition. The persona's L1 RAG layer is no
longer a collection of isolated modules — it's wired through
PersonaCognition with the recording decorator + swappable capture
sink in place.

Built with TDD discipline per Joel's directive — tests written
first describing the desired wiring, then implementation made each
pass.

persona/unified.rs:

- `admission: AdmissionState` → `Arc<AdmissionState>` so EngramSource
  can share it. Arc transparency means existing
  `cognition.admission.admit(...)` callers remain source-unchanged.

- New field `pub engram_source: Arc<dyn RagSource>` —
  RecordingRagSource<EngramSource> wrapping the real source. Bound
  to the persona's id at construction. PromptAssembly (slice 12+)
  consumes this as part of its source set.

- New field `pub capture_sink: Arc<dyn RagCaptureSink>` — defaults
  to NoopRagCaptureSink (zero overhead, drops events on the floor).
  Production callers swap in via PersonaCognition::with_capture_sink.

- New constructor `with_capture_sink(persona_id, persona_name,
  rag_engine, genome_budget_mb, capture_sink)` — full control over
  the sink. `new` and `with_budget` delegate to it with a default
  Noop sink.

TDD tests (all 6 pass; existing test_persona_cognition_defaults
unaffected):

- persona_cognition_has_engram_source — field exists, source_id
  is "engrams"
- default_capture_sink_is_callable_zero_cost — Noop sink accepts
  events without panic
- engram_admitted_surfaces_via_engram_source — pushes an engram
  via admission.push_for_test, calls engram_source.deliver,
  asserts the engram surfaces. PROVES the Arc<AdmissionState>
  sharing works end-to-end.
- capture_sink_records_engram_source_delivery — swaps in
  InMemoryRagCaptureSink at construction, calls deliver, asserts
  RecordingRagSource recorded a SourceDelivered event with
  source_id="engrams". PROVES the decorator wrapping works.
- default_noop_sink_drops_events — Noop sink path is exercised
  end-to-end without producing events
- test_persona_cognition_defaults — existing baseline test
  continues to pass (no regression)

Doctrine alignment:

- organization-purity-as-we-migrate: Arc transparency means no
  existing call sites need source changes; new fields are
  additive; clean no-backwards-compat seam
- substrate-is-a-good-citizen-on-the-host: NoopRagCaptureSink
  default keeps capture zero-cost; production opts in by swapping
  the sink at construction
- RTOS-brain-no-region-on-hot-path: field accesses are Arc-deref
  (no lock contention); engram_source.deliver runs sync inside
  its trait method
- persona-record-replay-is-a-product-requirement: capture is now
  reachable from PersonaCognition's surface; slice 12 PromptAssembly
  will use the engram_source through this field

What's next:

- Slice 12: PromptAssembly composes the engram_source +
  ConversationSource + RagBudgetManager + final prompt string;
  emits TurnStart / TurnEnd events around source calls so traces
  have full turn shapes
- Slice 12.5: airc rag-inspect <turn-id> operator CLI + golden-
  trace harness with semantic assertion DSL

References: memory persona-record-replay-is-a-product-requirement,
docs/architecture/EVERY-MODEL-INCLUDED-VIA-L1-BUDGET.md, the
existing PromptAssembly stub at persona/prompt_assembly.rs that
slice 12 fills in.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… airc transcript events (TDD, task #98)

Slice 10.6. Proves the RagSource trait composes against real-world
data sources beyond the in-process engram store. AircTranscriptReader
trait abstracts page_recent so unit tests don't need a running airc
daemon; implementation rides on airc_lib::Airc::page_recent directly
via orphan-rule-compliant impl in our crate.

persona/airc_source.rs (~480 lines, 10 tests, all green):

- AircTranscriptReader trait + AircRagSource (persona-bound, holds
  Arc<dyn AircTranscriptReader>, configurable fetch_limit)
- Recency-only ranking at slice 10.6 (1/(rank+1) score per event);
  salience-grade scoring against airc metadata is a future slice
- Text-only items at this fidelity — events with no body or non-
  text body are skipped (no clipping, no fabrication)
- Reader errors return empty delivery + tracing::warn; cognition
  stays up when airc subsystem is degraded
- Persona-scoped + cursor-scoped per the substrate's handle doctrine
- Continuation cursor opaque = {next_rank: N}; cross-persona /
  wrong-source cursors structurally refused

TDD: tests written first describing behavior with StubReader, real
impl made each pass. Tests cover: empty room, single text message,
non-text dropped, budget overflow → continuation, cross-persona ctx
refused, cross-persona cursor refused, wrong source_id cursor
refused, reader error returns empty with no panic, continuation
resumes from next rank, fetch_limit caps reader call.

Next: demo binary that exercises this against Joel's actual airc
daemon to show what a realistic RAG flow looks like with live
messages (per Joel: 'we should see a realistic rag for a given
context and plug into airc daemon').

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant