219 files, 2.0 MB, untracked in souveraine/docs and existing nowhere else. The volume is at 100% with no snapshots.
7.1 KiB
| task_id | title | status | assignee | priority | phase | references | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| turn-lifecycle-001 | Turn Lifecycle Events + Sensorium Completion — unblock Matrix outbound | scoping | high | 2.5 |
|
Task: Turn Lifecycle Events & Sensorium Completion
Status: already built vs still needed
A careful audit against the research shows the picture is different from what the task docs assumed.
What already exists ✅
TurnEventDispatcher (src/core/nervous/turn_dispatcher.rs) — fully built. 8 emit methods, 207 lines, 4 tests. Constructed in turn.rs:194 and wired at every integration point: reasoning chunks, tool start/end, stream segment, turn finish, primary complete, interrupt. It fires SensorEvents namespaced as turn:* onto the EventBus.
Missing from the dispatcher: emit_tool_call for the full tool call details (arguments), and emit_n1_start/emit_n1_end for the subconscious pass lifecycle. These are currently fired as raw SensorEvent::send() calls in turn.rs:618-627 and turn.rs:656-665 rather than going through the structured dispatcher.
Sensorium trait (src/core/sensorium/mod.rs) — already redesigned with the run(EventBus, CancellationToken) pattern. Methods align with letta-code's ChannelAdapter interface. SensoriumCoordinator manages spawn/shutdown lifecycle. 575 lines, 4 tests.
Stub sensoria — TuiSensorium::run() and MobileSensorium::run() both use run_event_loop() which subscribes to the EventBus. They're debugs-only but structurally correct.
What's actually missing ❌
-
MatrixSensorium — the concrete implementation. Inbound path is wired (SensoriumInputHandler). Outbound is stubbed:
send_messageexists on the trait but MatrixSensorium doesn't implement it meaningfully. -
Matrix client sharing — the
matrix_clientlives insiderun()'s closure. NeedOption<Arc<MatrixClient>>on the struct sosend_message()can reach it. -
Turn lifecyle events are sparse — the dispatcher emits
turn:segmentfor every 10-char chunk (fine), but there's no structured tool-call event with arguments (the TUI card needs it), and no N+1 lifecycle events through the dispatcher. -
No sensorium consumes turn events yet — TuiSensorium's
runjust logs them. The real TUI renders via ratatui'sFramedirectly, not through the EventBus. Matrix would be the first surface that genuinely drives itself from the event stream.
Research synthesis
| Harness | Pattern | What Souveraine already has |
|---|---|---|
| letta-code | ChannelPlugin → ChannelAdapter, lifecycle events via dispatchTurnLifecycleEvent() |
✅ Sensorium trait matches ChannelAdapter. TurnEventDispatcher matches lifecycle dispatch. |
| hermes-agent | BasePlatformAdapter(ABC) + GatewayStreamConsumer (sync→async queue bridge) |
✅ EventBus is the broadcast channel. Missing: a sync→async bridge for streaming (the LLM call is sync, the surface is async). |
| OpenHarness | StreamEvent union type, three callbacks per surface |
✅ BackendEvent is Souveraine's equivalent. The TUI consumes it. Matrix would consume SensorEvent off the bus instead. |
| jcode | Bus::global() + ServerEvent canonicalized stream |
✅ EventBus. TurnEventDispatcher is the canonicalizer. |
| letta-code upstream (new) | Custom webhook adapter — 130 lines, no inbound | ✅ Direct model for MatrixSensorium::send_message |
Key insight: three registries, not one
hermes-agent separates concerns cleanly:
- Platform Registry — what surfaces exist (Telegram, Matrix, CLI)
- Provider Registry — what LLM backends exist (OpenAI, Anthropic, Ollama)
- Transport Registry — how to speak each wire format (chat_completions, anthropic_messages)
Souveraine already has the Bifrost client as the single transport. The question is whether to build a provider registry analogous to hermes-agent's ProviderProfile + ProviderTransport when multi-provider BYOK lands. That's a separate scope decision — this task is about the surface side.
Scope
Part A: Tighten the TurnEventDispatcher (1 session)
- Add
emit_tool_call()with full arguments (for TUI card rendering via EventBus) - Move raw
SensorEvent::send()calls for N+1 start/end into the dispatcher (emit_n1_start(),emit_n1_end()) - Verify all integration points in
turn.rsuse the dispatcher consistently
Part B: MatrixSensorium outbound (2 sessions)
The concrete Matrix adapter that:
- Stores
matrix_clientasOption<Arc<MatrixClient>>on the struct (set byrun()) run()subscribes to EventBus forturn:*events keyed by this room'sconversation_idsend_message()posts via matrix_client- Streaming model: first chunk creates a new Matrix message via
send_message(), subsequent chunks calledit()viam.replaceat 150ms throttle (the letta-codestreaming.tspattern) handle_turn_event()dispatches by event type:turn:segment→ message edit,turn:reasoning→ thinking placeholder,turn:tool_start→ tool card
Part C: SensoriumCoordinator wiring (1 session)
Wire SensoriumCoordinator into LocalBackend so constructed sensoria actually run. Currently the coordinator exists but nothing calls register() or run_all().
Part D: Not in scope
- Provider registry / multi-provider BYOK — the Bifrost client stays as the single transport
- Lettа-style plugin discovery (
channel.json+plugin.mjs) — not needed until third-party surfaces - Declarative config schema (7 field types) — deferred until a settings UI needs it
- In-process Lite→Full upgrade for federation — deferred in federation-summon.md
Files affected
| File | Change |
|---|---|
src/core/nervous/turn_dispatcher.rs |
Add emit_tool_call, emit_n1_start, emit_n1_end |
src/backend/local/turn.rs |
Replace raw SensorEvent::send() for N+1 events with dispatcher calls |
src/core/sensorium/matrix/mod.rs |
Full MatrixSensorium impl (feature-gated) |
src/core/sensorium/matrix/client.rs |
Build client + sync loop |
src/core/sensorium/matrix/turn.rs |
MatrixTurn coordinator (port from your letta-code turn/) |
src/core/sensorium/matrix/stream.rs |
Streaming message edits via m.replace |
src/core/sensorium/matrix/blocks.rs |
Tool/thinking block rendering |
src/core/sensorium/matrix/html.rs |
Markdown → Matrix HTML formatter |
src/backend/local/mod.rs |
Wire SensoriumCoordinator |
src/core/sensorium/mod.rs |
Minor: TuiSensorium/MobileSensorium plumbing |
Build sequence
- Part A — tighten dispatcher. Pure additive, no risk. 4 new emit methods.
- Part C — wire coordinator. The Matrix sensorium has nothing to subscribe to otherwise.
- Part B — build MatrixSensorium. Blocked on Part A (the dispatcher is the event source) and Part C (the coordinator is the runtime harness). But the struct + client sharing + sync loop can be built independently and tested with
cargo test --features matrix.