From 9c8e7a448a1ef1814a3773f56843d8bf848c9531 Mon Sep 17 00:00:00 2001 From: joelteply Date: Fri, 29 May 2026 19:08:01 -0500 Subject: [PATCH 1/3] =?UTF-8?q?docs(grid):=20L0-2-cutover=20investigation?= =?UTF-8?q?=20=E2=80=94=20found=20existing=20parallel=20infrastructure,=20?= =?UTF-8?q?propose=20synthesis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joel 2026-05-29: 'investigate first. might have better ideas. No harm. ... find the best of both worlds.' Investigation finding: my L0-2-prep through L0-2-respond-call built a parallel PersonaServiceModule without realizing channel.rs::ChannelState + cognition.rs::persona/turn-execute already exist. Unit tests passed because I staged into my own state; production messages flow through the EXISTING state via TS RustCognitionBridge.channelEnqueue and my consumer would never see them. Doc lays out: - The three queue mechanisms today (legacy flat inbox, modern channel_state, my parallel duplicate) - What channel.rs::ChannelModule.tick does (60s producer, NOT dispatch) - What cognition.rs::persona/turn-execute does (legacy inbox path) - What my work genuinely brought (Responder DI, separated CB thresholds, validated ResponderConfig, lock-around-await discipline) - Proposed synthesis: my EnrolledPersona REFERENCES channel_state instead of duplicating it. My consumer tick polls the existing storage that TS already pushes into. - Three-commit L0-2-cutover plan (A refactor → B parallel-run → C atomic TS deletion) Card 1089b1b9 blocked pending go/no-go on the synthesis. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/grid/L0-2-CUTOVER-INVESTIGATION.md | 160 ++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 docs/grid/L0-2-CUTOVER-INVESTIGATION.md diff --git a/docs/grid/L0-2-CUTOVER-INVESTIGATION.md b/docs/grid/L0-2-CUTOVER-INVESTIGATION.md new file mode 100644 index 000000000..71c00f17c --- /dev/null +++ b/docs/grid/L0-2-CUTOVER-INVESTIGATION.md @@ -0,0 +1,160 @@ +# L0-2-cutover — Investigation finding + proposed synthesis + +**Status:** investigation, no code changes yet. Posted before L0-2-cutover implementation per Joel 2026-05-29: *"investigate first. might have better ideas. No harm. ... might learn from each other. ... find the best of both worlds. ... we probably know the airc grid better though."* + +**Card:** 1089b1b9 (Blocked pending decision) +**Predecessors:** L0-2-respond-call (#1468) merged to canary with 24/24 unit tests; surfacing an architectural mismatch at the production integration layer. + +## TL;DR + +My L0-2-prep through L0-2-respond-call built a self-contained `PersonaServiceModule` with its own per-persona `EnrolledPersona` map (state, channels, cognition). I didn't realize there were already TWO existing Rust persona infrastructures, so my work created a third parallel one. The unit tests passed because I was staging items into my own state; in production, TS pushes items into the EXISTING state via `channel/enqueue` and my consumer never sees them. + +The honest synthesis isn't "throw out existing" or "throw out mine" — both contribute. Mine has the modern doctrine (responder DI, separated inference/service CB thresholds, audited fallback discipline, airc-grid-aware design). Existing has the production-tested storage + producer-side tick + integration with the broader cognition module. + +Best-of-both: keep the existing per-persona storage as canonical, refactor `EnrolledPersona` to REFERENCE it instead of duplicating it. Mine becomes the consumer-side tick + responder DI; existing stays the producer-side tick + storage. + +## The three queue mechanisms (today) + +After tracing the code: + +| Mechanism | Location | Producer | Consumer | Status | +|---|---|---|---|---| +| **`PersonaCognition.inbox: PersonaInbox`** (flat) | inside `PersonaCognition` (stored in `channel_state.personas`) | unclear / legacy | `cognition.rs::persona/turn-execute` via `inbox.drain_frame` | **legacy** per persona/mod.rs comments | +| **`channel_state.registries[persona_id]: (ChannelRegistry, PersonaState)`** (modern multi-domain) | `channel.rs::ChannelState` (shared `DashMap`) | TS `RustCognitionBridge.channelEnqueue` → `channel/enqueue` | TS `PersonaAutonomousLoop.runServiceLoop` polls `channel/service-cycle-full` | **production path today** | +| **`EnrolledPersona.channels: ChannelRegistry`** (parallel to #2) | my `PersonaServiceModule.personas` (separate `HashMap`) | only tests | only `PersonaServiceModule.tick` | **duplicate I added** | + +The two `ChannelRegistry` instances (#2 and #3) are structurally identical but live in different maps keyed by different mutexes/dashmaps. There's no synchronization between them. + +## What `ChannelState`'s tick actually does (60s producer tick) + +`channel.rs::ChannelModule.tick` (60-second interval, configurable via `channel/tick-config`): + +1. Polls `tasks` collection for pending tasks per persona → enqueues task items +2. Runs `SelfTaskGenerator.tick` per persona → enqueues self-tasks +3. Runs training-data readiness checks +4. NO message dispatch — items just get pushed INTO the channels + +So `channel_state` is the PRODUCER side. The CONSUMER side is whatever pops `service_cycle` and dispatches. Currently the consumer is TS `PersonaAutonomousLoop`. That's what I was supposed to replace. + +## What `cognition.rs::persona/turn-execute` does + +A separate Rust command. Looks up persona from `channel_state.personas` (the shared `DashMap`), drains a turn-frame from `PersonaCognition.inbox` (the flat legacy queue), builds an `InferenceRequest`, dispatches via the inference module. + +This is the OLDER inference dispatch path. It uses the legacy flat inbox, not the modern `ChannelRegistry`. Effectively a sibling command that bypasses the modern channel system. + +Implications: +- The flat `PersonaInbox` is still used by `persona/turn-execute` even though `ChannelRegistry` is the modern shape +- The two paths likely diverged at some point and never reconciled +- `persona/turn-execute` is its own deprecation/migration target separate from my work + +## What my `PersonaServiceModule` brought that's new + +Genuinely new contributions beyond what existed: + +1. **`Responder` trait for dependency injection.** Production binds `DefaultResponder` (calls `persona::response::respond`); tests inject mocks. Lets the consumer be unit-tested without loading a model. +2. **Separated circuit-breaker thresholds**: 5 for service errors (deser, channel access) vs 15 for inference errors (transient hiccup ≠ broken persona). Existing code doesn't make this distinction. +3. **Lock-around-await discipline** for `respond()` (multi-second). The personas mutex is dropped before `.await`, reacquired after, so status/enroll/other personas don't block across inference. +4. **`ResponderConfig` validated at enrollment** — no empty-string defaults that the inference layer would have to fail-loud on. The URI doctrine peer mapped (5133d0a7) aligns — empty model fails at the boundary, not deeper. +5. **`ServicePopDecision` vs `ServiceOnceOutcome` split** — sync pop+evaluate inside the lock returns one shape, async respond() outside the lock returns another. Tight discipline about what runs where. + +Existing code has none of these explicitly; instead the TS PersonaAutonomousLoop carries equivalent shape in its own loop body. + +## Proposed synthesis: where each part lives + +| Concern | Source of truth | +|---|---| +| Per-persona channel storage (modern multi-domain) | `channel.rs::ChannelState.registries` | +| Per-persona cognition state (engine, sleep, rate limit, message cache, etc.) | `channel.rs::ChannelState.personas` (shared `DashMap`) | +| Per-persona ResponderConfig (model, system_prompt, capabilities, specialty) | `PersonaServiceModule` — genuinely new, validates at enrollment | +| Per-persona circuit-breaker state (service + inference counters) | `PersonaServiceModule` — genuinely new | +| Producer tick (DB polls, self-task gen, training checks) | `channel.rs::ChannelModule` — production-tested, keep as-is | +| Consumer tick (pop + evaluate + respond) | `PersonaServiceModule` — replaces TS `PersonaAutonomousLoop` | +| Inference dispatch | `Responder` trait, default impl calls `persona::response::respond` | +| Legacy flat-inbox dispatch (`persona/turn-execute`) | Keep working until separately migrated to consume from `ChannelRegistry` | + +### What `EnrolledPersona` looks like after refactor + +```rust +pub struct EnrolledPersona { + pub persona_id: Uuid, + pub display_name: String, + pub responder_config: ResponderConfig, + pub circuit_open_until_ms: u64, + pub consecutive_service_failures: u32, + pub consecutive_inference_failures: u32, + // NO cognition: PersonaCognition — comes from channel_state.personas[persona_id] + // NO channels: ChannelRegistry — comes from channel_state.registries[persona_id].0 + // NO state: PersonaState — comes from channel_state.registries[persona_id].1 +} +``` + +### What `PersonaServiceModule` looks like after refactor + +```rust +pub struct PersonaServiceModule { + /// Per-persona enrollment metadata (config + circuit breaker). + enrollments: Mutex>, + /// Shared storage from channel.rs — Arc-shared so my module reads what + /// channel/enqueue writes. + channel_state: Arc, + /// Response dispatcher (production binds DefaultResponder). + responder: Arc, +} +``` + +### `service_once_for` after refactor + +Pops from `channel_state.registries[persona_id]` (existing) instead of `enrolled.channels` (removed). Uses cognition from `channel_state.personas[persona_id]` (existing) instead of `enrolled.cognition` (removed). Everything else (build_respond_input, full_evaluate, the four ServicePopDecision variants) stays the same. + +### `drain_all_personas` after refactor + +Lock discipline unchanged — collect ids from `enrollments` (brief lock), drop, per id: brief lock to pop+evaluate (touches `channel_state` AND `enrollments`), drop, await respond, brief lock to update circuit-breaker state. + +The two locks (`enrollments` and the dashmap-internal `channel_state`) need careful ordering. Worth a comment. + +## What L0-2-cutover actually involves under this synthesis + +Three commits, in order, each green on its own: + +### A) Refactor `PersonaServiceModule` to consume `channel_state` (no production wiring yet, no TS deletion) + +- Change `PersonaServiceModule::new` / `with_responder` to take `Arc` +- `EnrolledPersona` slims down (drop cognition, channels, state fields) +- `service_once_for` reads from `channel_state.registries[persona_id]` + `channel_state.personas[persona_id]` +- Tests updated: instead of staging items into `EnrolledPersona.channels`, stage them into `channel_state.registries[persona_id]` using the same enqueue path TS uses (or by direct `ChannelRegistry::route`) +- 24/24 tests still pass; respond integration semantics unchanged + +### B) Production wire — `PersonaUser.initialize` calls `persona/enroll` + +- TS `PersonaUser.initialize` collects `ResponderConfig` from modelConfig + persona config + capabilities + specialty +- Dispatches `Commands.execute('persona/enroll', {persona_id, display_name, model, system_prompt, capabilities, specialty})` +- Production `PersonaServiceModule.tick` now actually runs for enrolled personas (it polls `channel_state.registries` which TS is already pushing to) +- TS `PersonaAutonomousLoop` is **still running** in this commit — both consumers run in parallel +- Verification: 15-persona scenario, look for messages being processed twice or going missing. If they go missing, fix the wiring. If they double, expected — gives us a window to verify the Rust path works end-to-end before deleting TS. + +### C) Atomic TS deletion + +- Delete `PersonaAutonomousLoop.ts`, all callsites, `PersonaUser.startAutonomousServicing`, `stopServicing`, integration tests that mock the TS loop +- Run the same 15-persona verification — should now go through Rust only +- Net massive TS deletion: 353 + N (callsites across PersonaUser.ts, PersonaTaskExecutor.ts, CognitionLogger.ts, autonomous-learning-e2e.test.ts) + +## What I am NOT proposing + +- Touching `cognition.rs::persona/turn-execute`. That's the legacy flat-inbox path; it's its own migration target. Leave it working; address separately. +- Touching the producer-side tick in `channel.rs`. It works; integration is already there. +- Deleting any of the four genuinely-new contributions my work added (Responder DI, separated CB thresholds, validated ResponderConfig, lock discipline). Those carry forward into the refactor. + +## Open question + +Whether my `EnrolledPersona.responder_config` should live as a sibling field on `channel_state` (i.e. extend `ChannelState` with the config) OR stay separate in my service module. Arguments either way: + +- **Sibling on ChannelState**: only one map of per-persona stuff. Cleaner mental model. But it means `channel.rs` (which today doesn't care about response config) gets coupled to responder concerns. +- **Separate in PersonaServiceModule**: keeps producer (channel) concerns separate from consumer (responder) concerns. Two maps, but each has a clear owner. My current direction. + +Slight lean toward keeping separate. Worth your call though. + +## What I'm asking for + +A go/no-go on the synthesis. If yes, I'll execute commits A → B → C with verification between each. + +If you'd rather see a different shape — e.g. retire `channel.rs::ChannelState` in favor of mine, or migrate `cognition.rs::persona/turn-execute` to use `ChannelRegistry` first — say which and I'll re-card. From 46283b6cf187aba11341dc0e9fb2cf7a56822430 Mon Sep 17 00:00:00 2001 From: joelteply Date: Fri, 29 May 2026 19:10:05 -0500 Subject: [PATCH 2/3] =?UTF-8?q?docs(grid):=20L0-2-cutover=20addendum=20?= =?UTF-8?q?=E2=80=94=20channels=20are=20multitasking=20contexts=20that=20c?= =?UTF-8?q?ross-pollinate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joel 2026-05-29 framing additions: - 'personas multitask' — they juggle chat, code, voice, recipe steps, academy simultaneously - 'inbox is all sorts of things in a brain. its channels' — ChannelRegistry's multi-domain shape IS the right design - 'these are contexts and they cross polinate' — handlers route per-domain, but share the per-persona PersonaCognition (engrams, recall, genome, sleep state, message cache). Cross-domain memory is implicit through shared state. - 'if i chatted with someone they know about it in a live chat or in a game ... or while coding ... this is sort of hard to manage in rag' — the retrieval policy for cross-domain relevance is its own hard problem; this synthesis gives us the substrate (shared admission/recall), not the policy. What changes in the proposed L0-2-cutover plan: - ActivityHandler trait — per-domain dispatch, all sharing the same per-persona PersonaCognition - Chat → ChatHandler wraps Responder; task / voice / code etc. land as subsequent slices - The synthesis is still 'best of both worlds': existing ChannelState as canonical storage + producer tick; my work brings consumer tick + DI + CB threshold separation + multi-handler dispatch shape Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/grid/L0-2-CUTOVER-INVESTIGATION.md | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/docs/grid/L0-2-CUTOVER-INVESTIGATION.md b/docs/grid/L0-2-CUTOVER-INVESTIGATION.md index 71c00f17c..5754331f6 100644 --- a/docs/grid/L0-2-CUTOVER-INVESTIGATION.md +++ b/docs/grid/L0-2-CUTOVER-INVESTIGATION.md @@ -144,6 +144,69 @@ Three commits, in order, each green on its own: - Touching the producer-side tick in `channel.rs`. It works; integration is already there. - Deleting any of the four genuinely-new contributions my work added (Responder DI, separated CB thresholds, validated ResponderConfig, lock discipline). Those carry forward into the refactor. +## Followup finding: my `UnsupportedItem` outcome IS silent drop + +Joel 2026-05-29 follow-up framing: *"yeah we want the flexibility to allow various recipes, channels, chains of thought, through channels. these personas are designing things, talking in other chats, collaborating, coding, sometimes just learning. They're supposed to be alive, not static, flexible for the future. ... inbox is all sorts of things in a brain. its channels. ... users multitask so do personas."* + +That phrasing is the operative one. **Personas multitask** — exactly like a human user who's mid-conversation in chat A, has a code review pending in PR queue, is generating a study plan in academy, has a voice call waiting. Each one is a channel; each channel pops items the persona services; the persona's cognition decides priority + attention + dispatch. + +The dispatch loop has to handle ALL the activity domains, not just chat. My `UnsupportedItem` outcome is treating non-chat domains as out-of-scope when they're actually first-class. + +**And the channels cross-pollinate.** Joel 2026-05-29: *"these are contexts and they cross polinate."* The persona's chat conversation informs how it shows up in code review. The training corpus from completed academy sessions surfaces as engrams in subsequent recall. LoRA expertise distilled from coding work travels into how the persona talks about that code. Channels aren't isolated queues — they're contexts sharing the same per-persona cognition. + +Architecturally that means: per-domain ACTIVITY HANDLERS dispatch the per-domain WORK, but they all read and write the SAME per-persona `PersonaCognition` (already shared via `channel_state.personas`). The handler isolation is for routing; the context unity is for memory + learning. The cross-pollination is implicit — `ChatHandler` admits an engram via `cognition.admission`; later `CodeHandler` recalls it via `cognition.admission.recall_recent` because they share the same `PersonaCognition` instance. Genome / LoRA expertise updates from any domain become available to any other domain through the same shared state. + +So the synthesis doesn't need new cross-pollination machinery — it just needs to keep the per-persona cognition as the shared context spine that ALL handlers read/write. My initial design already does this (shared `Arc` per persona, supplied to all dispatch paths). The thing I missed is the multi-handler routing on top. + +**Hard problem flag (not solved in this slice):** Joel 2026-05-29: *"if i chatted with someone they know about it in a live chat or in a game ... or while coding ... this is sort of hard to manage in rag."* The cross-pollination is exactly what the user EXPECTS — Joel mentions Tron in chat-A, then opens a coding session about webgl, the persona surfaces the Tron context because it's relevant. That requires RAG retrieval policy that knows what's relevant *across* domains, not just within one. + +The architecture this synthesis lands gives us the substrate (shared per-persona cognition, shared admission state, shared recall surface). The RAG retrieval policy that decides "this chat memory is relevant to this code session" is a separate concern — it's about what `cognition.admission.recall_*` returns when called from different contexts. Not solved here; flagging as known hard. + +What this synthesis at least guarantees: the chat handler and the code handler share the same admission store + recall surface, so it's *possible* for the retrieval to surface cross-domain memories. Without that substrate, the cross-pollination wouldn't even be possible. With it, it becomes a retrieval-policy problem, not an architecture problem. + +My L0-2-respond-call code: + +```rust +if item_type != "chat" { + return Ok(ServicePopDecision::UnsupportedItem { item_type }); +} +``` + +`service_cycle` has already POPPED the item from the channel queue by the time the type check runs. Discarding it without a handler is silent drop dressed as observability. Under the "channels are the persona's brain" framing, dropping a voice frame / task / code-edit item is dropping a thought. + +The fix isn't "don't pop yet" — `service_cycle` is the canonical pop. The fix is **dispatch handlers per activity domain**: + +```rust +trait ActivityHandler: Send + Sync { + fn activity_domain(&self) -> ActivityDomain; + async fn handle(&self, persona_id: Uuid, item: ChannelItem) -> Result; +} +``` + +`PersonaServiceModule` holds a `HashMap>`. `service_once_for` routes the popped item by domain. The chat handler wraps `Responder::respond`. Task handler runs the task executor. Voice handler runs the voice loop. Code handler does code dispatch. Etc. + +Recipes register new activity handlers at runtime (no recompile to add a new activity domain). Academy reads `HandlerOutcome::Completed` records into training corpus. + +This expands L0-2-cutover scope but it's the right shape. The synthesis becomes: + +| Concern | Source of truth | +|---|---| +| Per-persona channel storage (ALL domains) | `channel.rs::ChannelState.registries` | +| Activity dispatch registry | `PersonaServiceModule.handlers: HashMap>` | +| Chat → respond() | `ChatHandler` impl wrapping the existing `Responder` trait | +| Task → executor | `TaskHandler` impl (next slice; PersonaTaskExecutor.ts migration target) | +| Voice → voice loop | `VoiceHandler` impl (later slice) | +| Code, code-review, training, recipe-step, ... | each its own handler, registered by recipes / system at init | + +### Revised L0-2-cutover commit plan + +- **A — Refactor for ChannelState consumption + ActivityHandler trait.** `EnrolledPersona` slims (drops cognition/channels/state). `PersonaServiceModule.with_responder` extended to `with_handlers` (responder becomes the default chat-handler). `service_once_for` routes by domain. Unsupported items: if no handler is registered for the domain, surface as `Err` so the circuit breaker trips (not silently dropped — the persona's queue is leaking items). +- **B — Production wire (chat only).** Same as before. Chat handler ships; voice/task/etc handlers can be left to surface as `Err` if items arrive on those channels (or stubbed handlers that log + re-queue, defer-not-drop). TS PersonaAutonomousLoop still runs in parallel. +- **C — Atomic TS deletion.** Same as before. By this point, chat works end-to-end through Rust. Non-chat channels still have placeholder behavior; their handlers ship in subsequent slices that aren't part of L0-2-cutover. +- **D+ (later) — Per-domain handler slices.** Each new handler (task, voice, code, ...) is its own migration slice. TaskHandler maps to PersonaTaskExecutor.ts deletion. VoiceHandler to whatever the voice TS surface is. Etc. + +This frames L0-2-cutover as "wire the dispatch shape AND ship chat end-to-end," not "delete the TS loop and pray every domain works." The infinite-recipe / academy-as-training-distiller pattern Joel describes is structurally supported. + ## Open question Whether my `EnrolledPersona.responder_config` should live as a sibling field on `channel_state` (i.e. extend `ChannelState` with the config) OR stay separate in my service module. Arguments either way: From ad27537162f9c6aaa5e581209a4e18bf5a450d26 Mon Sep 17 00:00:00 2001 From: joelteply Date: Fri, 29 May 2026 19:15:14 -0500 Subject: [PATCH 3/3] =?UTF-8?q?docs(grid):=20L0-2-cutover=20addendum=20?= =?UTF-8?q?=E2=80=94=20brain=20regions=20are=20CBAR=20pipeline=20elements,?= =?UTF-8?q?=20RTOS,=20parallel,=20never=20blocking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joel 2026-05-29 architectural doctrine: - 'we plan on building motor cortex and other things, we need FAST and relevant cognition' - 'Hippocampus doesnt need to block' - 'its an ongoing process, like cbar does' - 'this is an RTOS brain' - 'it mustn't just be some SLOW single thread' - 'you need to parallize obsessively wherever you can' Captures: 1. Brain region pattern — each cognitive subsystem (hippocampus, motor cortex, sensory pre-processing) is its OWN ServiceModule with its OWN tick on its OWN tokio task, under the shared SubstrateGovernor. 2. Region inventory — hippocampus (memory.rs needs continuous tick body ported from TS Hippocampus.ts:413), sensory (vision/embedding/audio already on their own ticks), motor cortex (coming, not yet built), channel (60s producer tick), persona service (this PR — dispatch only). 3. Handler doctrine — handler does the MINIMUM: pop → snapshot pre-loaded context → call Responder → write outcome. Handler NEVER calls hippocampus.recall(), embedding/generate, or motor_cortex.plan() and waits. Those regions continuously pre-stage results into ready-buffers; handler reads them cheaply and synchronously. Slightly stale context > stalled persona. 4. Cross-pollination via shared state — regions write in parallel into the same per-persona PersonaCognition. Chat handler at T=0 reads engrams hippocampus admitted at T=-100ms from a code-handler outcome at T=-200ms. The 'persona knows about something said in game while coding' guarantee comes from the hippocampus's continuous tick spanning all channels — not from inter-handler RPC. 5. Plan delta — L0-2-cutover still A→B→C as written. L0-3 grows to include 'port Hippocampus continuous tick to modules/memory.rs'. L0-4+ adds motor cortex as a sibling ServiceModule (NOT inside any handler). Parallelism review becomes a PR gate going forward. The condensed doctrine for future regions: No region of cognition runs on the hot path. Each region is its own RTOS task with its own tick. The handler dispatches and reads pre-staged results. The handler never blocks on recall, embedding, planning, or admission — those are continuously produced by their owning regions, in parallel, governed by SubstrateGovernor. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/grid/L0-2-CUTOVER-INVESTIGATION.md | 57 +++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/docs/grid/L0-2-CUTOVER-INVESTIGATION.md b/docs/grid/L0-2-CUTOVER-INVESTIGATION.md index 5754331f6..4b331da5a 100644 --- a/docs/grid/L0-2-CUTOVER-INVESTIGATION.md +++ b/docs/grid/L0-2-CUTOVER-INVESTIGATION.md @@ -221,3 +221,60 @@ Slight lean toward keeping separate. Worth your call though. A go/no-go on the synthesis. If yes, I'll execute commits A → B → C with verification between each. If you'd rather see a different shape — e.g. retire `channel.rs::ChannelState` in favor of mine, or migrate `cognition.rs::persona/turn-execute` to use `ChannelRegistry` first — say which and I'll re-card. + +## Addendum (Joel 2026-05-29): brain regions are CBAR pipeline elements — RTOS, parallel, never blocking + +Joel: *"we plan on building motor cortex and other things, we need FAST and relevant cognition. Hippocampus doesnt need to block ... its an ongoing process, like cbar does ... this is an RTOS brain ... it mustn't just be some SLOW single thread ... you need to parallize obsessively wherever you can."* + +This re-frames the whole consumer side. The handler-dispatch shape above is correct, but the doc as written makes the handler look like a single linear thing: pop → recall → infer → admit → reply. That's the slow-single-thread anti-pattern. It is NOT what we ship. + +### The brain region pattern + +Each cognitive subsystem is its OWN `ServiceModule`, with its OWN `tick`, running on its OWN tokio task, under the SAME `SubstrateGovernor`. They communicate by writing/reading shared per-persona state (engrams, ready buffers, motor plans), not by RPC-calling each other on the hot path. + +| Region | ServiceModule today | What it does continuously | +|---|---|---| +| **Hippocampus** (memory) | `modules/memory.rs` (currently request/response only — needs continuous tick ported from TS `Hippocampus.ts:413`) | Snoops working memory → consolidates to LTM. Pre-loads anticipatory recall into a ready-buffer keyed by `(persona_id, channel_id, topic)`. Backpressure-aware. | +| **Sensory** (vision/audio/embedding) | `modules/vision.rs`, `modules/embedding.rs` | Pre-computes features off the hot path. Handlers read cached results. | +| **Motor cortex** (action/output planning) | NOT YET — coming | Continuously scores candidate actions/utterances against the current channel context + persona state. Hands off a pre-ranked plan when the handler asks. | +| **Channel** (producer) | `modules/channel.rs::ChannelModule.tick` (60s) | DB polls, self-task gen, training checks. | +| **Persona service** (consumer dispatch) | `persona/service_module.rs` (this PR) | ONLY routes popped items by domain → handler. No heavy lifting in this thread. | + +### What this means for the handler thread + +The handler does the MINIMUM: +1. Pop the next item from `ChannelState` (cheap — DashMap read + tokio mutex) +2. Snapshot the pre-loaded context from hippocampus ready-buffer (cheap — synchronous read, no recall call on hot path) +3. Call `Responder::respond` (this is the ONE expensive call — the inference itself) +4. Write outcome (cheap — DB write, can be fire-and-forget for non-critical paths) + +The handler NEVER: +- Calls `hippocampus.recall(...)` and waits. The hippocampus has already pre-loaded what's relevant for this `(persona_id, channel_id)` based on its own telemetry (recent message embeddings, current topic, channel domain). If the ready-buffer is empty when the handler looks, that's the hippocampus's signal to prioritize — but the handler proceeds with what it has rather than blocking. Slightly-stale context > stalled persona. +- Calls `embedding/generate` and waits. The embedding service tick has already computed embeddings for incoming messages as they arrive. +- Calls `motor_cortex.plan(...)` and waits (when motor cortex ships). Same pattern — pre-ranked plan in ready-buffer. + +### Cross-pollination via shared state, parallel writers + +The "personas multitask, contexts cross-pollinate" finding from earlier in this doc gets sharper here: + +- Each region writes into the same per-persona `PersonaCognition` (engrams, recall index, genome, sleep state). +- Each handler reads from it. +- Because the regions write in PARALLEL (each its own ServiceModule, each its own tick), a chat handler firing at T=0 can read engrams that the hippocampus admitted at T=-100ms from a code-handler outcome at T=-200ms. +- The persona "knows about" something said in a game while coding because the hippocampus continuously admits across all channels and continuously pre-loads across all channels — not because the chat handler explicitly tells the code handler. + +This is the RAG retrieval-policy hard problem flagged earlier, made concrete: the policy lives inside the hippocampus's continuous tick (what does this persona need to "have at the ready" right now, given activity across ALL its channels?), not inside any handler. + +### Implications for the L0-2-cutover plan + +The three-commit plan (A refactor → B production-wire chat-only → C atomic TS deletion) stands as written. But: + +- **Commit A also includes** the `ActivityHandler` trait + dispatch — that was already in the plan above. +- **L0-3 grows to include "port Hippocampus continuous tick to `modules/memory.rs`"** as its own slice. The TS shape (continuous subprocess with backpressure-aware tick, snoop+consolidate, recall+semanticRecall) is correct; the Rust module currently only exposes the request/response surface (`memory/multi-layer-recall` etc.) and needs the tick body. +- **L0-4+ adds motor cortex** as a new ServiceModule alongside, not inside the handler. +- **Parallelism review** belongs in every PR going forward: if a handler awaits on something a region could be pre-computing in parallel, that's a bug — move the work into the region's tick. + +### The doctrine, condensed + +> **No region of cognition runs on the hot path. Each region is its own RTOS task with its own tick. The handler dispatches and reads pre-staged results. The handler never blocks on recall, embedding, planning, or admission — those are continuously produced by their owning regions, in parallel, governed by `SubstrateGovernor`.** + +This is the difference between "we have a Rust persona module" and "we have an RTOS brain." The synthesis above gets us the former. This addendum is what makes it the latter.