Skip to content

feat(runtime): real Prefix dispatch for ArtifactSelector via dedicated path on MessageBus#1343

Merged
joelteply merged 1 commit into
canaryfrom
feat/runtime-artifact-prefix-dispatch
May 16, 2026
Merged

feat(runtime): real Prefix dispatch for ArtifactSelector via dedicated path on MessageBus#1343
joelteply merged 1 commit into
canaryfrom
feat/runtime-artifact-prefix-dispatch

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

Summary

Follow-up to #1339 (CBAR-PIECE-2 PR-3 — artifact dispatch via bus). Closes the Prefix-selector gap that PR-3 pinned as a no-op.

What this fixes

PR-3 routed ArtifactSelector::Exact through the bus's standard glob_matches path, which works for Exact but fails for Prefix: glob_matches splits on : not /, so Prefix(\"cognition/\") matches nothing through the existing matcher. PR-3 emitted warn! and pinned the no-op with a regression test.

What this changes

  • Add MessageBus::subscribe_artifact(selector, module_name) — sibling to MessageBus::subscribe but routes via ArtifactSelector::matches (Exact / Prefix on the full slash-convention key) instead of the colon-segmented glob_matches.
  • MessageBus::publish walks the artifact subscriber list in addition to the event subscriber list. Two coexisting matchers on the same publish path:
    • event_subscriptionsglob_matches (colon-segmented, unchanged)
    • artifact_subscriptionsArtifactSelector::matches (full key)
  • Runtime::register routes ALL ArtifactSelector variants (Exact AND Prefix) through subscribe_artifact. No more warn!, no separator translation, no PR-3-shaped gap.
  • Delivery stays synchronous through the dedicated path — on_artifact_available is contract-bound to cheap-and-return.

Why a dedicated path instead of unifying the separator

ArtifactKey convention is <module>/<surface>.<event> (slash + dot); the event bus convention is <a>:<b>:<c> (colon-segmented). They're semantically different — events are colon-segmented for per-segment globbing (data:*:created), artifacts are slash/dot-structured for module/surface namespacing without glob semantics. ArtifactSelector::matches is the right matcher for the latter; glob_matches is the right matcher for the former. Forcing one to fit the other would muddy both.

Test plan

Stack

🤖 Generated with Claude Code

…ssageBus

Follow-up to #1339 (CBAR-PIECE-2 PR-3 — artifact dispatch via bus).

What this fixes
- PR-3 routed ArtifactSelector::Exact through the bus's standard
  glob_matches path, which works for Exact but fails for Prefix:
  glob_matches splits on `:` not `/`, so Prefix("cognition/") matches
  nothing through the existing matcher. PR-3 emitted warn! and pinned
  the no-op with a regression test.

What this changes
- Add MessageBus::subscribe_artifact(selector, module_name) — sibling
  to MessageBus::subscribe but routes via ArtifactSelector::matches
  (Exact / Prefix on the full slash-convention key) instead of the
  colon-segmented glob_matches.
- MessageBus::publish now walks the artifact subscriber list in
  addition to the event subscriber list. Two coexisting matchers on
  the same publish path:
    event_subscriptions      → glob_matches (colon-segmented)
    artifact_subscriptions   → ArtifactSelector::matches (full key)
- Runtime::register routes all ArtifactSelector variants (Exact AND
  Prefix) through subscribe_artifact. No more warn!, no separator
  translation, no PR-3-shaped gap.
- Delivery is synchronous through the dedicated path because
  on_artifact_available is contract-bound to cheap-and-return.

Tests
- runtime/runtime.rs piece_2_pr3_dispatch_tests
  prefix_selector_currently_no_ops_pending_separator_unification
  renamed and flipped to
  prefix_selector_delivers_matching_keys_and_skips_others —
  verifies BOTH that the selector delivers matching keys AND that
  non-matching keys (different prefix) are correctly excluded.
- All 42 runtime:: tests pass (no regressions on the Exact, empty-
  subscriptions, or multi-module isolation tests).

Why a dedicated path instead of unifying the separator
- ArtifactKey convention is `<module>/<surface>.<event>` (slash +
  dot); the event bus convention is `<a>:<b>:<c>` (colon-segmented).
  They're semantically different — events are colon-segmented for
  per-segment globbing (`data:*:created`), artifacts are
  slash/dot-structured for module/surface namespacing without glob
  semantics. ArtifactSelector::matches is the right matcher for the
  latter; glob_matches is the right matcher for the former. Forcing
  one to fit the other would muddy both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joelteply joelteply merged commit 7d18581 into canary May 16, 2026
3 checks passed
@joelteply joelteply deleted the feat/runtime-artifact-prefix-dispatch branch May 16, 2026 22:16
joelteply added a commit that referenced this pull request May 16, 2026
)

* feat(cognition): audit-recorder (MODULE-CATALOG, claude-tab-1's #1 ranked module)

Per #1336 MODULE-CATALOG §VII `audit-recorder` row + claude-tab-1's
22:10Z broadcast ranking this as the cleanest place to start (~200 LoC,
no deps, unblocks the trace-bus landing for every downstream module).

PR-1 ships pure data + thin disk I/O + tamper-evident chain. PR-2
wires to MessageBus via the ArtifactSubscription surface that PIECE-2
PR-3 (#1339/#1343) just landed.

What ships in src/workers/continuum-core/src/cognition/audit.rs:

- AuditEntryKind enum: Refusal / GovernorOverride /
  FederationPolicyDrift / AccessDenied. ts-rs kebab-case wire.
- AuditEntry struct: seq + timestamp_ms + kind + payload (serde_json::
  Value with ts(type=unknown)) + chain_hash + prev_chain_hash.
  Tamper-evident: each entry's chain_hash references the previous
  entry's chain_hash, forming a SHA-256 chain.
- AuditChain: append-only writer with rolling hash state. new() for
  fresh chain; load(path) to resume from existing log; build_next() for
  the pure-derivation step; append() for the file-write helper.
- read_audit_log(path): replay + verify chain integrity. Three
  failure modes: ChainBroken (hash mismatch = tampering), SequenceGap
  (missing entries), TimestampWentBackward (clock skew on writer).
- AuditError: typed error with Display + std::error::Error + From for
  io::Error + serde_json::Error.

JSON-Lines file format (`audit.jsonl` — one entry per line). Easy to
grep, easy to tail. No external schema migration needed for new kinds.

Tamper-evidence design (NOT cryptographic signing, by intent):

  prev_chain_hash for entry N = chain_hash of entry N-1
  chain_hash for entry N = SHA-256(seq || ts || kind || payload || prev_chain_hash)
  Genesis prev_chain_hash = 64 zeros

Tampering with entry N invalidates entries N+1..end. Verifier catches
it on read with the typed ChainBroken error. Asymmetric signing
(prevents tampering rather than detecting it) lands when continuum-core
gets a per-node identity key — separate concern.

Tests: 19 passing on cargo test --lib --features metal,accelerate
cognition::audit::

- AuditEntryKind serializes kebab-case (4 variants)
- Fresh chain genesis: seq=0, prev_hash=GENESIS_HASH
- Seq increments monotonically
- Chain links: B.prev_chain_hash == A.chain_hash
- compute_chain_hash deterministic + sensitive to every input
- Append → read round-trips
- Many appends form valid chain
- Read nonexistent path returns empty (first-boot case)
- Load restores chain position from existing log
- Tampered payload breaks chain (THE point of the chain)
- Sequence gap detected
- Backward timestamp detected
- Equal timestamps accepted (fast writers)
- AuditError trait + From impls
- AuditEntry serde camelCase
- ts-rs export bindings (2: AuditEntry, AuditEntryKind)

VDD evidence N/A — pure-data + thin I/O. Evidence lands with PR-2
(MessageBus wiring) when actual events flow through.

Stack:
- This PR: pure data + chain + verifier
- Future PR-2: MessageBus subscription wiring (subscribe to RefusalAudit/
  GovernorOverride/FederationPolicyDrift/AccessDenied event types via
  ArtifactSubscription; emit AuditEntryRecorded)
- Future PR-3: asymmetric signing when per-node identity key lands

Coordination note: codex broadcast a claim for audit-recorder at
22:16:50Z while this PR was already 95% done; surfacing to airc to
avoid duplicate work + cede next module (threat-detector or
working-set-manager per the ranking).

* fix(cognition): keep audit append failure atomic

---------

Co-authored-by: Test <test@test.com>
joelteply added a commit that referenced this pull request May 16, 2026
…ierarchy + paging (#1346)

PR-1 of working-set-manager (MODULE-CATALOG §VII + GENOME-FOUNDRY-
SENTINEL Parts 2/3/4). Pure data + serde + ts-rs exports. No traits,
no I/O, no async, no wiring — those land in PR-2/PR-3.

Mirrors the slice shape that worked for CBAR-PIECE-2 PR-1 (#1321) +
PIECE-5 PR-1 (#1331): ship the data shape first, hang behaviors on
it incrementally.

What lands

- TierRole (Fast/Warm/Bench/Cold/Frozen) + is_present_on_uma helper
- EvictionPolicy + canonical_for(role) pinning the per-role policy
  table from GENOME-FOUNDRY-SENTINEL Part 2
- TierCapacity + available_bytes (saturating) + utilization (zero-safe)
- EvictionRecord (trace bus event shape — PR-3 wires through #1339+
  #1343 artifact dispatch)
- TierError + Display + Error
- PageKind / PageOffset (Whole / Expert / Range)
- PageRef { kind, artifact, offset } — Hash+Eq for HashMap-key use
- PageHandle (what page_in returns)
- ResidentPage + WorkingSetCapacity + WorkingSet
- PageFault + AccessDenied (typed events; audit-recorder #1344
  subscribes to AccessDenied as one of its inputs)
- PersonaId(Uuid) + ArtifactId(Uuid) typed newtypes — the type
  system catches swapped arguments at audit_access(persona, page)
  sites. Wire is transparent (UUID string).

What is deliberately deferred

- WorkingSetManager trait + page_in/page_out/audit_access (PR-2)
- TierStore trait + per-role impls (separate PR set)
- MMU permission table enforcement (PR-2 or PR-3)
- PageFault/EvictionRecord publishing via artifact dispatch (PR-3)
- Hardware-anchor Vec<TierConfig> from governor (substrate-governor
  lane — codex's #1345)

Tests

35 tests on genome:: pin every invariant the type system + serde
encoding guarantee. 35/35 pass. No regressions across other 2467
lib tests.

Clippy baseline bump 146→148 — drift from canary HEAD; the +2
warnings are NOT from genome code (zero clippy hits in genome/).
They land via codex's recent #1340/#1341/#1344/#1345 merges that
didn't bump the file. Bumping here so the ratchet stays meaningful
for the NEXT PR to gate against.

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joelteply added a commit that referenced this pull request May 16, 2026
…e traits (+sentinel cleanup) (#1353)

* feat(genome): working-set-manager PR-2 — WorkingSetManager + TierStore traits

PR-2 of working-set-manager (MODULE-CATALOG §VII + GENOME-FOUNDRY-
SENTINEL Parts 2/3/4). Trait surface on top of PR-1's typed data
layer (#1346). No implementations — those are PR-3 + the per-role
TierStore PRs.

Mirrors the slice shape: PR-1 = data, PR-2 = traits, PR-3 = impl +
wiring. Same pattern as CBAR-PIECE-2 (data #1321 → traits #1323 →
dispatch #1339+#1343) and PIECE-5 (data #1331 → loader #1333 →
probe #1335 → enforcement #1338).

What lands

- `genome::store::TierStore` — the trait every per-role tier
  implementation satisfies. Five methods: role / read / write /
  evict / capacity / observe_access. `Send + Sync + async_trait`
  for tokio concurrency. Used by working-set-manager (PR-3) as
  `Box<dyn TierStore>` per configured role.

- `genome::manager::WorkingSetManager` — the top-level paging
  interface. Four methods this PR: page_in / page_out / working_set
  / audit_access. The fifth method `check_permission(actor, region,
  op)` from GENOME-FOUNDRY-SENTINEL Part 4 lands in PR-3 alongside
  the GenomeRegion + Op type definitions.

- `genome::blob::ArtifactBlob` — bytes-side type for
  `TierStore::write`. Content-addressed via ArtifactId. NOT
  ts-rs-exported — large blobs don't belong on the TS wire.

- `genome::blob::Provenance` — PR-2 minimal stub (artifact_id +
  created_at_ms). Full GENOME-FOUNDRY-SENTINEL Part 1 shape grows
  this type later without breaking the trait surface.

Design refinements vs the raw spec

- `working_set` returns `Option<&WorkingSet>` instead of
  `&WorkingSet`. Unregistered persona → `None` instead of fabricating
  an empty struct that masks wrong-persona-id bugs.
- `page_in` returns `Result<PageHandle, PageFault>` per spec.
  Documented that PageFault is a typed observability signal, not a
  failure error — caller treats it as success-with-trace-event.

Tests

13 new tests on genome::manager + genome::store + genome::blob:
trait object-safety, dispatch through Arc/Box, audit_access denial
shape, ArtifactBlob size invariant, Provenance wire shape. 48
genome:: tests total (PR-1's 35 + PR-2's 13). No regressions across
the other 2487 lib tests.

Stack

#1339 / #1343 — CBAR-PIECE-2 PR-3 artifact dispatch (mine)
#1344 — audit-recorder (codex's, subscribes to AccessDenied)
#1346 — working-set-manager PR-1: data types (mine)
THIS PR — working-set-manager PR-2: traits (mine)
NEXT  — working-set-manager PR-3: per-persona impl + PageFault /
        EvictionRecord publishing via artifact dispatch path

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

* fix(sentinel): remove dead self_clone — was masking under -D warnings test build

Drift from canary HEAD: src/workers/continuum-core/src/modules/sentinel/mod.rs:1039
defined `let self_clone = Arc::new(self.sentinels.clone());` and never
referenced it. The actual clone used downstream is `let sentinels =
Arc::clone(&self.sentinels);` at line 1066 (now 1065 after this fix).

Why it bit me: the test build for genome PR-2 (#1346 stack)
`cargo test --lib --features metal,accelerate` is the gate the
prepush hook runs, and that build has -D warnings effectively-on for
unused_variables — so the warning became "error: could not compile."
This blocks every Rust-touching push until fixed.

Per Joel's boy-scout-rule + "Bugs from new users / new machines / new
OS are GIFTS — fix the source, never hack": dead-code fix in place,
sweeping as I go.

This is NOT genome-PR-2 scope but is REQUIRED for the precommit gate
to let genome-PR-2 through. Bundling here keeps the gate working;
splitting it into a separate PR would block PR-2's push behind a fix
that has nothing to do with PR-2's logic.

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

* fix(genome): scope uuid::Uuid import to test module in blob.rs

Earlier fix in this branch removed `use uuid::Uuid;` from file scope
because clippy on `cargo check --lib` flagged it unused. But the
TEST module uses `Uuid::nil()` — `cargo test --lib` failed with E0433
"use of undeclared type Uuid" once the test build saw the references.

Fix: move the import inside `#[cfg(test)] mod tests` so it lives where
it's used. Clippy on the non-test build sees no Uuid usage in
production code (correct — Provenance::minimal doesn't need it),
and the test build sees the import where the test fixtures need it.

48/48 genome:: tests pass after the fix.

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

---------

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joelteply added a commit that referenced this pull request May 16, 2026
…rocess impl (#1355)

PR-3 of working-set-manager. Hangs the per-persona behaviors on the
PR-1 data layer (#1346) + PR-2 trait surface (#1353). Pure local
implementation — no MessageBus integration baked in (the trait's
`page_in` Result already carries `PageFault` as the typed
observability signal; callers wire to the artifact dispatch path
#1339+#1343 themselves).

Mirrors the slice shape: PR-1 = data, PR-2 = traits, PR-3 = impl.
Same pattern as CBAR-PIECE-2 (data #1321 → traits #1323 →
dispatch #1339+#1343) and PIECE-5 (data #1331 → loader #1333 →
probe #1335 → enforcement #1338).

What lands

- `LocalWorkingSetManager` struct holding:
  - `Vec<Arc<dyn TierStore>>` — tier chain, ordered Fast → Frozen
  - `RwLock<HashMap<PersonaId, WorkingSet>>` — per-persona state
  - `RwLock<HashMap<PageRef, PersonaId>>` — page-ownership map
    for the MMU-style `audit_access` enforcement

- Four trait method impls:
  - `page_in` — fast-path resident hit, otherwise walks tier chain
    top-down, returns PageFault with typed from_role/to_role (None
    from_role = true cold miss; Some = tier promotion)
  - `page_out` — removes from working set, observes target tier,
    skips pinned pages silently, returns `TierError::RoleNotConfigured`
    if the target tier isn't in the configured Vec
  - `working_set` — returns None per refined contract (lock-guard
    escape impossible through the trait signature; tests use the
    `working_set_snapshot` helper instead)
  - `audit_access` — checks page_owners map; returns typed
    `AccessDenied` with full context (actor + owner + reason) on
    cross-persona read

- Two convenience methods:
  - `register_persona(persona, capacity)` — must be called before
    any page_in for the persona
  - `register_page_owner(page, owner)` — populates the MMU table

- Diagnostic helper:
  - `working_set_snapshot(persona)` — clones for telemetry + tests

Deliberately deferred (PR-4 or later)

- MessageBus integration for PageFault/EvictionRecord publishing.
  The trait's Result<PageHandle, PageFault> contract gives caller-
  side observability today; bus publishing can stay caller-side
  too (and the artifact dispatch I shipped in #1339+#1343 is the
  publishing path when callers wire it).
- Eviction policy invocation when target tier is at limit. PR-3
  returns NoEvictionCandidate; PR-4 wires the callback so the
  manager observes + re-publishes the EvictionRecord.
- `check_permission(actor, region, op)` — needs GenomeRegion + Op
  type definitions; lands with PR-4.

Refinements to the PR-2 trait contract

- `working_set` returns `None` because borrowing through the RwLock
  would expose the lock guard type and break the trait signature.
  Documented in the impl + the trait docstring. Tests + telemetry
  use `working_set_snapshot` (clone, not on hot path).

Tests

8 new tests on genome::local_manager:
- page_in_resident_returns_cached_without_tier_walk — hot-path
  correctness (whole point of a working set)
- page_in_walks_tier_chain_and_records_promotion — Fast → Bench →
  Cold walk order, PageFault.from_role + to_role correctness
- page_in_true_cold_miss_has_none_from_role — typed signal
  sentinel uses to distinguish "page never existed"
- audit_access_denies_cross_persona_read — typed AccessDenied
  with full context, same contract PR-2's trait test pins
- page_out_observes_target_tier_and_handles_unconfigured — typed
  RoleNotConfigured for "this hardware doesn't have that role"
- page_out_skips_pinned_pages_silently — composition pin contract
- working_set_snapshot_reflects_page_in_state — diagnostic helper
- tier_count_reflects_configured_tiers — O(1) governor diagnostic

56 genome:: tests total (PR-1's 35 + PR-2's 13 + PR-3's 8). No
regressions across other 2566 lib tests.

Stack

#1339 / #1343 — CBAR-PIECE-2 PR-3 artifact dispatch (mine)
#1344 — audit-recorder (codex's, subscribes to AccessDenied)
#1346 — working-set-manager PR-1: data types (mine)
#1353 — working-set-manager PR-2: traits (mine)
THIS PR — working-set-manager PR-3: per-process impl (mine)
NEXT  — PR-4: bus integration + eviction-callback wiring +
        check_permission + GenomeRegion/Op types

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joelteply added a commit that referenced this pull request May 17, 2026
… publishing helpers (#1358)

PR-4 of working-set-manager. Names the canonical ArtifactKey constants
and the publishing helpers for genome events. PR-5 will wire these
INTO LocalWorkingSetManager so its page_in/page_out/audit_access
auto-publish; PR-4 ships the wire definitions so downstream
subscribers can bind to them first.

Why split this from the LocalWorkingSetManager wiring (PR-5)

The wire shape is the coordination point between three modules:
- audit-recorder (#1344, codex) — subscribes to AccessDenied
- sentinel-observer (future) — subscribes to PageFault for learning
  access patterns
- demand-aligned-recall (future) — subscribes to PageFault for
  ResidencyHint caching

Naming the keys + helpers in their own PR locks the contract first.
Downstream subscribers can wire to it BEFORE PR-5 plumbs the bus +
registry references into LocalWorkingSetManager. Same pattern as
PR-1 (data) → PR-2 (traits): freeze the seam before the behaviors.

What lands

- Three canonical ArtifactKey constants under genome/:
  - PAGE_FAULT_KEY = "genome/working_set.page_fault"
  - EVICTION_RECORD_KEY = "genome/working_set.eviction"
  - ACCESS_DENIED_KEY = "genome/working_set.access_denied"

- Three async publishing helpers — serialize the typed event and
  publish through the artifact dispatch path I shipped in #1339 +
  #1343:
  - publish_page_fault(bus, registry, fault)
  - publish_eviction_record(bus, registry, record)
  - publish_access_denied(bus, registry, denied)

- subscribe_to_genome_events(bus, module_name) convenience — wires a
  module to all three keys via bus.subscribe_artifact (#1343 path).

- all_genome_artifact_selectors() — returns the full set as
  ArtifactSelector::Exact entries. Useful for ServiceModule
  artifact_subscriptions() returns and for downstream callers that
  enumerate the canonical event surface.

What is deliberately deferred (PR-5)

- Wiring the helpers INTO LocalWorkingSetManager so its trait method
  impls auto-publish after each call. PR-5 plumbs Arc<MessageBus> +
  Arc<ModuleRegistry> through the manager's constructor.
- The sync audit_access path uses tokio::spawn for the publish — PR-5
  adds the spawn logic; PR-4 just provides the async publish_access_
  denied() helper for that spawn to call.

Tests

7 new tests on genome::bus, all wiring the full #1339+#1343 dispatch
path end-to-end with the genome event types:

- artifact_keys_have_canonical_string_values — pins the canonical
  wire values so renames are deliberate
- all_genome_selectors_cover_every_key_as_exact — every key appears
  as Exact selector (not Prefix); adding a fourth key fails this
  test to force the author to verify the wire contract
- publish_page_fault_routes_to_subscribed_module — end-to-end
  Runtime + RecordingModule + publish dispatch, with serde round-trip
- publish_eviction_record_routes_to_correct_key — independence of
  keys, subscriber only sees its key
- publish_access_denied_routes_to_audit_input_key — the audit-
  recorder integration point (#1344's AccessDenied input)
- convenience_helper_subscribes_to_all_three_event_types — full
  firehose subscriber sees all three
- selective_subscriber_only_sees_its_subscribed_key — sentinel-
  observer that only wants page-faults isn't forced to filter

63 genome:: tests total (PR-1's 35 + PR-2's 13 + PR-3's 8 + PR-4's
7). No regressions across other 2582 lib tests.

Stack

#1339 / #1343 — CBAR-PIECE-2 PR-3 artifact dispatch + Prefix
follow-up (mine; the dispatch path PR-4 publishes through)
#1344 — audit-recorder (codex's, subscribes to AccessDenied)
#1346 — working-set-manager PR-1: data types
#1353 — working-set-manager PR-2: traits
#1355 — working-set-manager PR-3: LocalWorkingSetManager
THIS PR — working-set-manager PR-4: bus wire + helpers
NEXT  — PR-5: LocalWorkingSetManager auto-publish via these helpers

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joelteply added a commit that referenced this pull request May 17, 2026
…publishes via bus hook (#1362)

Closes the genome stack end-to-end. The artifact dispatch path I
shipped in #1339+#1343 + the PR-4 publishing helpers + the PR-3
LocalWorkingSetManager all wire together so a persona's `page_in` /
`audit_access` calls fan typed events out to subscribers (audit-
recorder #1344 on AccessDenied, future sentinel-observer on
PageFault, future demand-aligned-recall on PageFault).

What lands

- `LocalWorkingSetManager::with_bus(tiers, bus, registry)` —
  optional bus hook stored as `Option<BusHook>` on the manager.
  Constructed once at startup; switching publishing on/off mid-
  service would race + is not supported.

- Auto-publish on:
  - `page_in` returning `PageFault` (true cold miss OR tier
    promotion) → publishes via `publish_page_fault` under
    `PAGE_FAULT_KEY`
  - `audit_access` returning `AccessDenied` → publishes via
    `publish_access_denied` under `ACCESS_DENIED_KEY`
  - Both via `tokio::runtime::Handle::try_current().spawn(...)`
    — see "Why spawn instead of await" below.

- `LocalWorkingSetManager::new(tiers)` (PR-3 shape) preserved
  unchanged: bus-less mode for tests + standalone use.

- `Runtime::bus_arc()` accessor added — returns Arc<MessageBus>
  for long-lived publishers (like LocalWorkingSetManager wired
  via with_bus) that need to hold their own bus reference.

Why spawn instead of await

`bus.publish` walks DashMap subscriber lists; the DashMap's `Map`
trait impl is keyed by `&'static str` and that doesn't satisfy the
"for any lifetime" requirement when the call sits inside a Send-
bounded `async fn` (which `async_trait` generates for trait method
impls). Spawning into a tokio task decouples the publish from the
caller's Send-ness — the spawned future owns its Arc captures, no
borrow crosses the await boundary in the caller.

Sub-fix in MessageBus::publish

While debugging the lifetime issue, found that `MessageBus::publish`
held the DashMap borrow across the `await module.handle_event(...)`
call inside both its glob_matched + artifact_matched walks. That's
the actual root cause of the "DashMap is not general enough" error
when publish is called from spawn-contexts. Refactored both walks to
collect matching `module_name: &'static str` into a `Vec` first
(dropping the DashMap borrow), then await dispatch from the Vec.
Same semantics, no more borrow-across-await — `publish` is now safe
to call from any Send-bounded async context.

Tests

6 new tests on genome::local_manager::pr5 sub-section:

- page_in_true_cold_miss_with_bus_publishes_page_fault — end-to-end
  Runtime + RecorderModule + with_bus + page_in → spawn → publish →
  subscriber. Yields with tokio::task::yield_now in a bounded loop
  to let the spawn complete (no fixed sleep).
- page_in_tier_promotion_with_bus_publishes_correct_fields —
  from_role/to_role correctness through the spawn path.
- page_in_resident_hit_with_bus_does_not_publish — resident-hit
  path stays silent (no noisy events for hot pages).
- audit_access_denial_with_bus_publishes_via_spawn — same spawn
  pattern, but from the sync audit_access trait method.
- audit_access_allowed_with_bus_does_not_publish — only denials
  are observable events.
- bus_less_mode_does_not_publish_but_methods_work — backwards-
  compat for the standalone `new(tiers)` constructor.

69 genome:: tests total (PR-1's 35 + PR-2's 13 + PR-3's 8 + PR-4's
7 + PR-5's 6). All pass, no regressions across other 2615 lib tests.

The MessageBus refactor is a load-bearing improvement to the bus
itself — any future caller that wants to publish from a Send-bounded
spawn context (which is most non-trivial integration code) benefits.
Caught it on the genome integration; landing the fix here keeps the
stack reviewable as one slice.

Stack

#1339 / #1343 — CBAR-PIECE-2 PR-3 artifact dispatch + Prefix
follow-up (mine; the dispatch path PR-5 publishes through)
#1344 — audit-recorder (codex's, now wired-in via AccessDenied)
#1346 — working-set-manager PR-1: data types
#1353 — working-set-manager PR-2: traits
#1355 — working-set-manager PR-3: LocalWorkingSetManager
#1358 — working-set-manager PR-4: bus keys + publishing helpers
THIS PR — working-set-manager PR-5: auto-publish wiring (this is
        the architectural payoff of the whole genome stack)

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joelteply added a commit that referenced this pull request May 18, 2026
…shing helpers (#1392)

PR-3a of inference-llm. Same pattern as my genome::bus PR-4
(#1358): name the canonical ArtifactKey constants + ship the async
publishing helpers + subscriber convenience. The actual real-engine
integration lands in PR-3b/PR-4; PR-3a ships the bus surface so
downstream observers (sentinel-observer, VDD harness, audit-recorder)
can wire to it today before the engine swap.

What lands

Four canonical ArtifactKeys under inference/:
- INFERENCE_REQUEST_KEY = "inference/llm.request"
- INFERENCE_COMPLETE_KEY = "inference/llm.complete"
- FIRST_TOKEN_EMITTED_KEY = "inference/llm.first_token"
- RESIDENCY_FAULT_KEY = "inference/llm.residency_fault"

Four async publishing helpers — serialize the typed event + publish
through the artifact dispatch path (#1339 + #1343):
- publish_inference_request
- publish_inference_complete
- publish_first_token_emitted
- publish_residency_fault

Three subscriber-convenience surfaces:
- subscribe_to_inference_responses(bus, name) — most observers want
  outcomes (complete + first_token + fault), not requests
- inference_response_selectors() — three Exact selectors
- all_inference_selectors() — four selectors including request for
  full-firehose consumers (audit-recorder when it covers inference)

Design choices

- Two subscriber surfaces (response-only vs full firehose) because
  most observers don't want every request — they want outcomes.
  Audit-recorder + VDD harness may want the firehose for the
  prod-replay chain Joel pushed at #1385.
- Request key INFERENCE_REQUEST_KEY in the publish helpers but NOT
  in the default observer set. Producers (persona-cognition) emit
  requests; observers see responses. Wiring symmetry without the
  noise.
- Same naming convention as genome::bus (module/surface.event) for
  cross-module consistency.

What is deliberately deferred (PR-3b / PR-4)

- Wiring helpers INTO InferenceLlmModule::handle_command so it
  auto-publishes after each call. PR-3b plumbs Arc<MessageBus> +
  Arc<ModuleRegistry> through the module's constructor.
- Real LLM engine (LlamaCppAdapter integration) — PR-4
- InferenceRequest artifact subscription (module subscribes to
  requests via bus instead of going through command bus) — needs
  persona-cognition to publish via bus first

Tests

7 new tests on inference::llm_module_bus:
- keys_have_canonical_string_values (pin wire strings)
- response_selectors_cover_three_keys_as_exact
- all_selectors_cover_four_keys
- publish_inference_complete_routes_to_subscribed_module
  (end-to-end through artifact dispatch)
- each_publish_helper_routes_to_its_own_key
- response_only_subscriber_does_not_see_requests
- full_firehose_subscriber_sees_requests_too

7/7 pass. No regressions across other 2958 lib tests.

Stack

- #1387 — inference-llm PR-1: typed event surface
- #1391 — inference-llm PR-2: ServiceModule impl (stub-backed)
- THIS PR — inference-llm PR-3a: bus keys + publishing helpers
- NEXT — PR-3b: InferenceLlmModule auto-publishes via these helpers
  after each handle_command call
- THEN — PR-4: real LlamaCppAdapter invoke + tokenizer + streaming

Co-authored-by: Test <test@test.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