Skip to content

feat(continuum-core/persona): L0-2-prep — PersonaSlot extension, enroll opens#1464

Merged
joelteply merged 2 commits into
canaryfrom
grid/l0-2-prep-impl
May 29, 2026
Merged

feat(continuum-core/persona): L0-2-prep — PersonaSlot extension, enroll opens#1464
joelteply merged 2 commits into
canaryfrom
grid/l0-2-prep-impl

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

First implementation slice of L0 per L0-PERSONA-COGNITION-E2E-PLAN.md. Builds on L0-1's minimum unit (#1457).

What

PersonaServiceModule now carries persona slots. Each enrolled persona has a PersonaSlot holding its PersonaCognition (the per-persona container for engine + inbox + rate_limiter + sleep_state + adapter_registry + genome + classifier + caches + admission state from persona::unified).

  • PersonaSlot struct: persona_id, display_name, cognition, circuit_open_until_ms, consecutive_failures
  • PersonaServiceModule: personas: Mutex<HashMap<Uuid, PersonaSlot>> + shared Arc<RagEngine> (so all enrolled personas share the retrieval substrate)
  • enroll(persona_id, display_name) — constructs cognition under shared RagEngine, stores slot. Idempotent on persona_id; updates display_name but preserves existing cognition + circuit-breaker state (silently resetting cognition would be a fallback)
  • persona/status now reports the enrolled list (id + display_name + total)
  • persona/enroll command opens (was: L0-2-not-wired error). Loud validation: missing persona_id, missing display_name, malformed uuid all fail with named errors

What does NOT change

  • tick is still a no-op. The TS PersonaAutonomousLoop continues to drive the production loop. service_once_for + dispatch wiring lands in L0-2-dispatch.
  • No TS deleted yet. PersonaAutonomousLoop.ts deletion lands in L0-2-cutover after dispatch is proven.

Why safe to ship alone

The Rust enrollment is latent — enrolling a persona changes no production behavior because the production loop still runs TS-side. When L0-2-dispatch wires service_once_for, the slot machinery is already proven by these tests.

Tests — 10 passing

Test What it pins
config_declares_persona_prefix_and_high_priority module config
status_with_no_enrollments_reports_zero_and_prep_scope status shape
enroll_constructs_slot_and_status_reflects_it enroll happy path
enroll_is_idempotent_and_updates_display_name idempotency, no silent cognition reset
enroll_two_distinct_personas_keeps_both multi-enrollment
enroll_missing_persona_id_fails_loud input validation
enroll_missing_display_name_fails_loud input validation
enroll_invalid_uuid_fails_loud input validation
unknown_command_returns_clear_error unknown commands
tick_is_no_op_in_prep_slice tick still inert

Verified on Xcode 26.3 + llama/metal feature.

🤖 Generated with Claude Code

joelteply and others added 2 commits May 29, 2026 15:32
…cognition path

Joel 2026-05-29: 'would take careful planning to migrate. I would get
e2e persona cognition first, within RUST alone.'

Plan covers:
- What 'e2e persona cognition in Rust alone' means concretely (the
  cognition decisions + state stay Rust; ingress/egress can stay
  transitional TS)
- Audit of what already runs in Rust (PersonaCognition,
  PersonaCognitionEngine, full_evaluate, respond, service_cycle,
  PersonaServiceModule L0-1 minimum)
- Audit of what still runs in TS (PersonaAutonomousLoop driving the
  loop today, PersonaMessageEvaluator orchestrating, etc.)
- Five sub-slices:
  - L0-2-prep: PersonaSlot extension + open enroll (no dispatch)
  - L0-2-dispatch: service_once_for wired, exercised in tests only
  - L0-2-cutover: atomic TS-loop deletion + Rust-loop activation
  - L0-3: genome paging moves to Rust
  - L0-4: inbox routing moves to Rust
  - L0-5: final PersonaUser.ts cull
- Dependencies + blockers explicitly: NOT blocked by airc#1075 or
  e51ab14e (uses universal CommandExecutor's existing TS-route
  branch); BLOCKED by knowing the rag_engine source — open question
  to investigate before L0-2-prep code

Pre-implementation investigation (4 items) called out so the next
PR after this is on solid ground.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ll opens

Builds on L0-1's minimum unit (#1457). Each enrolled persona gets a
PersonaSlot carrying its PersonaCognition (the per-persona container
for engine + inbox + rate_limiter + sleep_state + adapter_registry +
genome + classifier + caches + admission state from persona::unified).

What changes:
- PersonaSlot struct (persona_id, display_name, cognition,
  circuit_open_until_ms, consecutive_failures)
- PersonaServiceModule now carries personas: Mutex<HashMap<Uuid, PersonaSlot>>
  + rag_engine: Arc<RagEngine> (held at module level so all enrolled
  personas share retrieval substrate)
- enroll(persona_id, display_name) — constructs PersonaCognition under
  the shared RagEngine, stores the slot. Idempotent on persona_id
  (updates display_name; preserves existing cognition + circuit-breaker
  state — silently resetting cognition would be a fallback)
- persona/status now reports the enrolled list (snapshot of id +
  display_name + total count) instead of the L0-1 zero stub
- persona/enroll command (was: returns L0-2-not-wired error). Parses
  persona_id (uuid) + display_name from JSON params, calls enroll(),
  reports the new total
- Loud validation: missing persona_id, missing display_name, malformed
  uuid all fail with named errors. No silent defaults.

What does NOT change:
- tick is still a no-op. The TS PersonaAutonomousLoop continues to
  drive the production loop. service_once_for + dispatch wiring lands
  in L0-2-dispatch.
- No TS deleted yet. PersonaAutonomousLoop.ts deletion lands in
  L0-2-cutover after dispatch is proven.

Why this is safe to ship alone:
The Rust enrollment is *latent* — enrolling a persona changes no
production behavior because the production loop still runs TS-side.
When L0-2-dispatch wires service_once_for, the slot machinery is
already proven by the L0-2-prep tests.

Tests: 10 passing.
- config_declares_persona_prefix_and_high_priority
- status_with_no_enrollments_reports_zero_and_prep_scope
- enroll_constructs_slot_and_status_reflects_it
- enroll_is_idempotent_and_updates_display_name
- enroll_two_distinct_personas_keeps_both
- enroll_missing_persona_id_fails_loud
- enroll_missing_display_name_fails_loud
- enroll_invalid_uuid_fails_loud
- unknown_command_returns_clear_error
- tick_is_no_op_in_prep_slice

Verified on Xcode 26.3 + llama/metal feature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joelteply joelteply merged commit ca4dbc3 into canary May 29, 2026
3 checks passed
@joelteply joelteply deleted the grid/l0-2-prep-impl branch May 29, 2026 20:44
joelteply added a commit that referenced this pull request May 29, 2026
…h full_evaluate (#1465)

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>
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