Skip to content

feat(continuum-core/persona): L0-2-dispatch — service_once_for through full_evaluate#1465

Merged
joelteply merged 1 commit into
canaryfrom
grid/l0-2-dispatch-impl
May 29, 2026
Merged

feat(continuum-core/persona): L0-2-dispatch — service_once_for through full_evaluate#1465
joelteply merged 1 commit into
canaryfrom
grid/l0-2-dispatch-impl

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

Second implementation slice of L0-PERSONA-COGNITION-E2E-PLAN.md. Builds on L0-2-prep (#1464).

What

Each EnrolledPersona now carries its own ChannelRegistry + PersonaState, and the service module wires the dispatch path through the unified pre-response gate.

  • Slot rename: PersonaSlotEnrolledPersona (collided with existing cognition::response_orchestrator::PersonaSlot DTO)
  • service_once_for(persona, now_ms) — pops via channels.service_cycle, deserializes the chat item (local ChatItemWire matching the camelCase to_json output), builds a FullEvaluateRequest, calls full_evaluate, returns the decision as ServiceOnceOutcome
  • drain_all_personas(now_ms) — iterates enrolled personas, calls service_once_for up to MAX_DRAIN_PER_TICK (20) per persona, manages per-persona circuit breaker (5 failures → 30s cooldown)
  • tick now calls drain_all_personas
  • ServiceOnceOutcome enum: Idle / Evaluated{message_id, decision} / UnsupportedItem{item_type} — voice + task items surface as UnsupportedItem rather than silently dropped (anti-fallback)

Production safety

  • No production code calls persona/enroll yet. The runtime invokes tick() every 250ms but with zero enrolled personas it's a no-op.
  • L0-2-cutover will atomically (a) wire persona/enroll from production, (b) delete PersonaAutonomousLoop.ts, (c) make Rust the production driver.

What does NOT change yet

  • No call to respond() — that needs upstream TurnContext + room history + known-specialties roster (lives in PersonaMessageEvaluator today). Follow-up slice wires this with the upstream context plumbed through.
  • No TS deletions yet.

Tests — 16 passing (10 prep + 6 new)

Test What it pins
service_once_for_idle_returns_idle empty channel → no work
service_once_for_dispatches_chat_item_through_full_evaluate full happy path: enqueue → pop → evaluate
drain_all_personas_processes_two_personas_independently multi-persona isolation
drain_respects_max_drain_per_tick per-tick cap
tick_with_no_enrolled_personas_succeeds_quietly empty tick is no-op
tick_with_enrolled_persona_and_no_items_is_no_op enrolled but idle is no-op

Verified on Xcode 26.3 + llama/metal feature.

🤖 Generated with Claude Code

…h full_evaluate

Builds on L0-2-prep (#1464). Each EnrolledPersona now carries its own
ChannelRegistry + PersonaState, and the service module has the dispatch
path wired through the unified pre-response gate.

Why the slot rename:
- L0-2-prep introduced `service_module::PersonaSlot` which collided
  with the existing `cognition::response_orchestrator::PersonaSlot`
  (a minimal identity+specialty DTO used as input to respond()).
- Renamed mine to `EnrolledPersona` — clearer name AND no collision.

What changes:
- EnrolledPersona extends with channels: ChannelRegistry + state: PersonaState
  (initialized fresh in enroll)
- service_once_for(persona, now_ms) — pops via channels.service_cycle,
  deserializes the chat item (local ChatItemWire struct matching the
  camelCase to_json output), builds a FullEvaluateRequest, calls
  full_evaluate, returns the decision as ServiceOnceOutcome
- drain_all_personas(now_ms) — iterates enrolled personas, calls
  service_once_for up to MAX_DRAIN_PER_TICK (20) per persona, manages
  per-persona circuit breaker (5 consecutive failures → 30s cooldown)
- tick now calls drain_all_personas
- ServiceOnceOutcome enum: Idle | Evaluated{message_id,decision} |
  UnsupportedItem{item_type} — voice + task items surface as
  UnsupportedItem rather than silently dropped (anti-fallback)

Production safety:
- No production code calls persona/enroll yet. The runtime invokes
  tick() every 250ms but with zero enrolled personas it's a no-op.
- L0-2-cutover will atomically (a) wire persona/enroll from production,
  (b) delete PersonaAutonomousLoop.ts, (c) make Rust the production
  driver of the loop.

What does NOT change yet:
- No call to respond() — that needs upstream TurnContext + room
  history + known-specialties roster that lives in PersonaMessageEvaluator
  today. Follow-up slice wires respond() with the upstream context
  plumbed through.
- No TS deletions yet.

Constants:
- CIRCUIT_BREAKER_MAX_CONSECUTIVE_FAILURES: 5
- CIRCUIT_BREAKER_COOLDOWN_MS: 30_000
- MAX_DRAIN_PER_TICK: 20

Tests: 16 passing (10 L0-2-prep + 6 new dispatch tests).
- service_once_for_idle_returns_idle
- service_once_for_dispatches_chat_item_through_full_evaluate
- drain_all_personas_processes_two_personas_independently
- drain_respects_max_drain_per_tick
- tick_with_no_enrolled_personas_succeeds_quietly
- tick_with_enrolled_persona_and_no_items_is_no_op

Verified on Xcode 26.3 + llama/metal feature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joelteply joelteply merged commit 80cf6ee into canary May 29, 2026
3 checks passed
@joelteply joelteply deleted the grid/l0-2-dispatch-impl branch May 29, 2026 21:23
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