Skip to content

feat(continuum-core/persona): PersonaServiceModule — L0-1 singleton Rust ServiceModule#1457

Merged
joelteply merged 1 commit into
canaryfrom
continuum-core-airc-embed
May 29, 2026
Merged

feat(continuum-core/persona): PersonaServiceModule — L0-1 singleton Rust ServiceModule#1457
joelteply merged 1 commit into
canaryfrom
continuum-core-airc-embed

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

Roadmap: L0-1 of GRID-MIGRATION-ROADMAP (#1442). First proof point of the persona-Rust migration end-to-end.

Why

Joel directives 2026-05-29: rust core, nodejs is web only, AI persona under Rust domain, TS personas were CPU-killing. PersonaUser.ts ran every cadence tick through V8 → IPC → Rust → IPC → V8 dispatch; with 15 personas, hundreds of IPC round-trips/sec on the JS event loop. This collapses to ONE Rust tick.

What

Two pieces on one branch:

continuum-core::airc module (scaffold)

  • AircHandle wrapping airc_lib::Airc for in-process embedding
  • ContinuumAdapter implementing airc_lib::adapter::ConsumerAdapter (from airc PR Add host capability probe so resolver actually runs in production #1075) for forge.continuum.event.v1
  • 3 unit tests pin the adapter routing contract
  • Stubbed on_envelope — translation into continuum's command_bus / persona inbox is the follow-up

continuum-core::persona::service_module (L0-1 done)

  • Singleton PersonaServiceModule owning ALL personas' service cycles in-process
    • One tick loop, per-persona drain bound, per-persona circuit breaker
    • HashMap<Uuid, PersonaSlot> for the fleet
    • persona/enroll + persona/status commands
  • Calls existing channel_registry::service_cycle() directly — zero IPC, zero V8 blocking
  • 8 unit tests, all passing on Xcode 26.3 + llama metal feature

Why singleton instead of per-persona module

ModuleConfig.name: &'static str — runtime registry can't store dynamic names. Beyond the constraint, singleton wins anyway: one tick = whole fleet, adding the 16th persona is enrollment-only.

Verified end-to-end

  • cargo check -p continuum-core --lib --features llama/metal: clean (Xcode 26.3, llama.cpp submodule initialized)
  • cargo test persona::service_module: 8/8 passing
  • cargo test airc: 3/3 passing

Path-dep note (revert before merge)

Cargo.toml temporarily points the airc-lib path at /Users/joel/.airc/worktrees/9c63f3d8 (the #1075 ConsumerAdapter branch). Once airc PR #1075 merges to rust-rewrite, revert to the sibling ../../../../airc/crates/airc-lib path before final merge. This is a blocker — do not merge until #1075 + #1081 land in airc rust-rewrite.

What L0-1 does NOT yet do (follow-up cards)

  • L0-2 (card 7a45a15f): cognition dispatch wiring in service_once_for. Currently acks-and-discards items; needs per-persona PersonaContext (model, system_prompt, capabilities, recent_history) injected at enrollment. Slot extension follows.
  • L0-3 (card 392dafde tracked separately): remove dyn Any from ServiceModule trait. Current as_any impl is debt per Joel's no-Any directive.

Roadmap context

Layer Status
L0-1 This PR
L0-2 Card 7a45a15f filed
L0-3 Card filed under PersonaGenomeManager migration
L0-4 Card filed under PersonaInbox routing
L0-5 PersonaAutonomousLoop deletion (gates on L0-1..L0-4)

🤖 Generated with Claude Code

@joelteply joelteply force-pushed the continuum-core-airc-embed branch from e0c903d to 2054b0c Compare May 29, 2026 17:33
@joelteply joelteply changed the title feat(continuum-core): L0-1 PersonaServiceModule — Rust ServiceModule replacing TS PersonaAutonomousLoop + scaffold continuum-core::airc embed (cards 7a45a15f, ea1c01a4) feat(continuum-core/persona): PersonaServiceModule — L0-1 singleton Rust ServiceModule May 29, 2026
…erviceModule (L0-1)

Replaces TypeScript PersonaAutonomousLoop. ONE Rust tick services
every enrolled persona instead of N TS loops crossing the V8↔Rust
IPC boundary on every cadence beat.

Why singleton, not per-persona:
- ModuleConfig.name is &'static str — runtime registry can't store
  dynamic per-persona names.
- Beyond the constraint, singleton wins anyway: one tick = whole
  fleet, adding the 16th persona is enrollment-only, the cadence
  budget is shared across personas instead of per-persona contended.

Surface:
- enroll(persona_id, display_name) -> Result
- enrolled_count() -> usize
- ServiceModule impl with command_prefixes=["persona/"], High prio,
  250ms tick. Handles persona/status + persona/enroll.
- Per-persona circuit breaker (5 consecutive failures = 30s cooldown)
  + per-persona drain bound (20 items / tick) keeps one bad persona
  from starving the rest.

Tests: 8 unit tests covering config, status, enroll/idempotency,
multi-persona, unknown-command rejection, empty-tick, enrolled-tick.

Note on as_any: ServiceModule trait currently requires it for
downcasting in the registry; tracked separately for removal per the
no-Any directive.

L0-1 of GRID-MIGRATION-ROADMAP (PR #1442 merged into canary).
Follow-ups: L0-2 (cognition dispatch in service_once_for), L0-3
(genome manager), L0-4 (inbox routing), L0-5 (delete the TS
PersonaAutonomousLoop once L0-1..L0-4 land).

Verified on Xcode 26.3 + llama metal feature, all 8 tests pass.
No /Users paths, no private deps — all airc crates pinned at
workspace level to public CambrianTech/airc git revs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joelteply joelteply force-pushed the continuum-core-airc-embed branch from 2054b0c to dfd4e50 Compare May 29, 2026 17:46
@github-actions github-actions Bot added size: M and removed size: L labels May 29, 2026
@joelteply joelteply merged commit acbc698 into canary May 29, 2026
3 checks passed
@joelteply joelteply deleted the continuum-core-airc-embed branch May 29, 2026 17:51
joelteply added a commit that referenced this pull request May 29, 2026
…ll opens (#1464)

* docs(grid): L0 E2E persona cognition plan — sequencing the Rust-only 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>

* feat(continuum-core/persona): L0-2-prep — PersonaSlot extension, enroll 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>

---------

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