Build(deps-dev): Bump @typescript-eslint/eslint-plugin from 8.29.1 to 8.46.2#149
Closed
dependabot[bot] wants to merge 1 commit into
Closed
Conversation
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.29.1 to 8.46.2. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.2/packages/eslint-plugin) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.46.2 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
1c5e1d3 to
8883a6c
Compare
Contributor
Author
|
Superseded by #167. |
This was referenced Jun 2, 2026
Closed
joelteply
added a commit
that referenced
this pull request
Jun 3, 2026
…all (#1519) * refactor(persona): `&ctx`-pure RAG request + ctx-derived tracing span Elegance pass on the patterns the slice-13 work established. Per Joel 2026-06-02: "we are on sort of an elegance refactor and then for improved reliability and speed." What changed: 1. `RagInspectionRequest::for_ctx(&ctx, now_ms)` — new constructor that takes the persona context directly. Replaces the 4-arg `for_persona(persona_id, name, now_ms, &profile)` at the call site. `for_persona` stays (it's the underlying derivation) but new code uses `for_ctx` to honor the substrate's `&ctx` doctrine ([[context-is-the-client-airc-token-is-identity]]): hand the context, not its parts. 2. `PersonaContext::span()` — new method that returns a `tracing::info_span!` tagged with `persona_id`, `agent_name`, `peer_id`, `role`, `tier`, `ctx_len`, `model`. The span derives from `&ctx` — no manual field threading at every log call site. 3. `serve_persona_loop` rewritten in two layers: - Outer entry function wraps the inner future with `.instrument(ctx.span())`. Every log line inside the loop inherits the persona's identity fields automatically. - Inner function drops the `let persona_id = hosted.identity.x` extractions; reads `ctx.identity.peer_id` etc. directly at use sites. Two internal `tracing::warn!` lines lose their persona_id/agent_name fields (now inherited from the span); they keep just per-turn delta (`lamport`, `error`). Net effect: - Field extraction count in service_loop drops from 3 manual extracts + 4 redundant tracing field annotations to 0. - Log output gains persona_id + agent_name + role + tier + ctx_len + model on EVERY internal log line, automatically. The substrate's observability is now span-shaped, not manual. - New code that needs a derived RAG request just writes `RagInspectionRequest::for_ctx(ctx, now)` — one arg vs four. Why `.instrument` not `.entered`: - `Span::entered` returns a non-Send RAII guard; tokio spawned futures need Send. The two-function split (outer thin wrapper with `.instrument`, inner async function) is the standard tracing pattern for spans across awaits. Verification: - cargo build --lib --tests clean - cargo test persona::service_loop — 4 passed - cargo test persona::supervisor — 4 passed Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(persona/host): extract PersonaSpawnSupervisor + BootSummary Elegance pass — extract-class refactor pulling the 170-line inline boot composition out of `ipc/mod.rs::start_server` into a named class. Per Joel 2026-06-02: "Must have elegance obsessively. Like a Java dev. NO SHAME. It's better." What changed: 1. `PersonaSpawnSupervisor` struct (in `persona/host.rs`) owns the spawner / instance_manager / registry / factory / tier_id / model_registry / rt_handle inputs. Construct once at boot; call `.spawn_all(&mut provider)` to produce a `BootSummary`. 2. `BootSummary { hosted, failures }` + `BootSlotFailure { slot_index, role, persona_id, reason }` — typed result structs. Replace the inline `let mut hosted_count: usize = 0` / `let mut failed_count: usize = 0` counters with a real value type the substrate can publish (`persona:boot:summary` event — Q5 of the design doc, deferred to slice 13.5+) and downstream clients (web, jtag CLI) can read with the same shape per [[clients-are-rust-too-thin-node-web-shell]]. 3. The supervisor's `spawn_all` method handles every previously- inline concern: - `bootstrap_planned` failure → orderly-drain orphans + return summary with synthetic failure row - `materialize_adapters` with runtime_lookup closure (so `ctx.runtime` is populated from the registry) - Per-slot `spawn_and_attach` private method handles `spawn_persona_service` + `attach_service_loop` + handle drain on attach-failure (the BLOCKER 1/2 fixes from PR #1511 are preserved, just relocated) 4. IPC boot collapses from ~170 lines of inline code to ~30 lines: construct supervisor → spawn task → build provider → call `supervisor.spawn_all(&mut provider).await` → log summary. 5. Helper `supervisor_error_facts` centralizes pulling `(slot_index, role)` out of `SupervisorError`'s two variants — the kind of trivial-but-DRY private fn Java/dotnet shops write without apology. Why this matters (the doctrine): - The IPC server boot concern and the persona spawn concern had different lifetimes and different test needs. Mixing them in one function violated "one logical decision, one place" ([[compression-principle]]). - `PersonaSpawnSupervisor` is now unit-testable in isolation. The IPC server's test surface shrinks. Slice 14's RoleAwareProvider + multi-persona work has one named insertion point. - `BootSummary` is the structured event payload the design doc's Q5 named. Once `RoleId` derives `TS` (slice 14), the struct gets the ts-rs export and web/jtag clients read it directly per the Rust-first-clients doctrine. Verification: - cargo build --lib --tests clean - cargo test persona::host — 2 passed (BootSummary attempted + serde camel-case) - cargo test persona::supervisor — 4 passed (unchanged) - cargo test persona::service_loop — 4 passed (unchanged) - IPC boot composition shrinks ~140 lines; supervisor's spawn_all is now the single named extraction point for slice 13.5 / 14 changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(persona): AircCitizen trait — drop Option<Arc<PersonaAircRuntime>> from PersonaContext (#144) Java-style "extract interface" on the substrate's airc-handle. Slice 13.5 elegance pass per Joel 2026-06-02 ("Must have elegance obsessively. Like a Java dev. NO SHAME"). Before: PersonaContext.runtime: Option<Arc<PersonaAircRuntime>>. The Option existed solely for test fixtures that couldn't easily build a real PersonaAircRuntime; production code paid .expect("None is test-only") on the hot path. After: PersonaContext.runtime: Arc<dyn AircCitizen>. Tests use a typed StubAircCitizen. Production upcoerces from PersonaAircRuntime, which now impls AircCitizen + AircTranscriptReader. Rust 1.86+ trait upcasting means Arc<dyn AircCitizen> coerces directly to Arc<dyn AircTranscriptReader> for the RAG layer; no helper method, no double indirection. Trait surface (minimum viable): - fn peer_id(&self) -> Uuid - async fn subscribe(&self) -> Result<EventStream, AircError> - async fn say(&self, text: &str) -> Result<EventId, AircError> - AircTranscriptReader as supertrait (page_recent for the RAG layer) What changed: - persona/airc_citizen.rs (new): AircCitizen trait + StubAircCitizen. - persona/airc_runtime.rs: PersonaAircRuntime impls AircCitizen + AircTranscriptReader; delegates to its internal Arc<Airc>. - persona/supervisor.rs: PersonaContext.runtime drops the Option. materialize_adapters' runtime_lookup signature is now Option<Arc<dyn AircCitizen>>; missing runtime surfaces as typed SupervisorError::RuntimeMissing { slot_index, role, persona_id } per [[no-fallbacks-ever]]. - persona/airc_persona_conversation.rs: takes Arc<dyn AircCitizen>, calls trait methods directly (no runtime.airc() detour). - persona/host.rs: spawn_persona_service drops the .expect; host's runtime_lookup upcoerces PersonaAircRuntime to AircCitizen for materialize_adapters. - persona/service_loop.rs fake_hosted: runtime is now Arc::new(StubAircCitizen::new(peer_id)) instead of None. - bin/airc_chat_demo.rs: dropped the Some(_) wrapping — Arc<PersonaAircRuntime> auto-coerces to Arc<dyn AircCitizen>. Doctrine: - [[personas-are-citizens-airc-is-identity-provider]]: AircCitizen IS the substrate's actor type — same trait for personas, humans (#142 BaseUser), browsers. The persona is one citizen; the human- via-jtag is another; the Claude-Code session is another. - [[no-fallbacks-ever]]: no Option, no .expect, no silent default. RuntimeMissing is a typed error with persona_id named. - [[context-is-the-client-airc-token-is-identity]]: PersonaContext IS the &ctx. Same shape compiles in tests + production. - [[clients-are-rust-too-thin-node-web-shell]]: AircCitizen is the typed Rust primitive future jtag-CLI / web client / native client bind to. Foundation for task #142 (BaseUser hierarchy) — each variant will carry Arc<dyn AircCitizen> + kind-specific extensions (cognition for persona, WebAuthn for human, tab state for browser). Test plan: - cargo build --lib --no-default-features --features livekit-webrtc,llama/mac-cpu-only — clean. - cargo test --lib ... persona:: — 705/706 pass (the one flake is persona::evaluator::tests::test_all_gates_pass_normal_message, an unrelated CPU-jitter timing assertion that passes in isolation). - Integration trace: deferred to PR-time verification. Closes #144. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(architecture): LIFE-OF-A-PERSONA + source/drain anchor — close onboarding gap surfaced by external review Two doc changes from an outside-perspective review (Gemini) of the substrate, triaged per [[external-llm-reviews-extract-themes-discard-citations]] — specific PR citations were fabricated, but two themes were real: 1. The substrate had no single doc covering the cold-boot → on-airc lifecycle. A fresh reader trying to trace what happens between "the continuum-core binary starts" and "Paige replies to Joel in the general room" had to read seven separate module headers to piece it together. 2. "Source/drain doctrine" was used in COGNITION-CACHE-HIERARCHY.md without anchoring what the drain actually IS — readers had to infer. What changed: - docs/architecture/LIFE-OF-A-PERSONA.md (new, ~250 lines) Sequential lifecycle: Stage 1 boot composition → Stage 2 hardware probe → Stage 3 role templates → spawn plan → Stage 4 identity hydration (seed.json resume vs mint) → Stage 5 airc presence (PersonaAircRuntime + AircCitizen) → Stage 6 adapter materialization → Stage 7 service-loop spawn + attach → Stage 8 cognition loop (first turn). Every stage names its Rust module + typed failure mode. Closes the operational onboarding gap. Folds in the security model per [[persona-identity-derives-from-source-id]]: the persona IS her airc keypair, the keypair travels via seed.json, the host hardware has a SEPARATE identity. No central identity broker. Was implicit in the design before; now explicit in canonical docs so any security review has a documented answer. - docs/architecture/COGNITION-CACHE-HIERARCHY.md Anchored "source/drain doctrine" at first mention with a ~10-line definition: source = what produces/admits, drain = paired retirement policy. Linked to memory [[source-drain-is-the-universal-pattern]]. Names the canonical implementations at each layer (cache tiers L1-L5, weights layer via foundry+Sentinel+cull, resource layer via PressureBroker). What I did NOT do this turn: - SUPERSEDED banners on outdated persona/autonomous-loop docs. Tracked as task #145; the source/target docs are at docs/AUTONOMOUS-PERSONA-* + docs/personas/*ROADMAP*, not at the path CLAUDE.md cites. Wants its own focused audit. - "Citizen" anchor in CBAR/GENOME-FOUNDRY-SENTINEL canonical docs. Less load-bearing once persona/airc_citizen.rs (this branch's refactor) provides the Rust-side anchor. - Floor-vs-ceiling resolution paragraph in INFERENCE-LANES-REALISTIC. Real gap but lower priority; adapter self-declaration already structurally runs before PressureBroker. Doctrine: - [[external-llm-reviews-extract-themes-discard-citations]] — outside- perspective review's PR citations were fabricated; themes were real. Discard citations; engage with themes. - [[read-existing-docs-before-writing-new-ones]] — both edits surface pre-existing doctrine that wasn't documented at the canonical-doc layer. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(persona/supervisor): lock in SupervisorError::RuntimeMissing behavior (review #1513) Address reviewer finding: the AircCitizen extraction added `SupervisorError::RuntimeMissing` but no test asserted it actually fires when `runtime_lookup` returns None. Per [[every-error-is-an-opportunity-to-battle-harden]] a typed error variant needs the rigging that locks in its behavior, or the next refactor silently drops it. Two tests added to `supervisor::tests`: 1. `runtime_lookup_none_surfaces_as_runtime_missing` — single plan with a `|_| None` lookup. Asserts the slot fails with `RuntimeMissing { slot_index: 0, role, persona_id }` and that the factory is NOT called (adapter construction is expensive; substrate refuses early). 2. `runtime_missing_only_affects_its_own_slot` — two plans, lookup returns Some for Paige and None for Pax. Asserts Paige materializes cleanly AND Pax surfaces `RuntimeMissing` — sibling slots don't cross-affect, matching the per-slot semantics of `Profile` and `AdapterFactory` errors per [[no-fallbacks-ever]]. Both tests verified locally: 6/6 supervisor tests pass. Reviewer: https://github.com/CambrianTech/continuum/pull/1513#issuecomment-4606231586 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(persona): PersonaConversation::prime — move airc subscribe off the cognition hot path (#146) Per Joel 2026-06-02: "Most latency goes to reinit or time spent with memory/disk... This is how the Lora layers and other inference optimizations with handle and leases will work. Same goes for serialization and other inefficiencies. Copy by ref don't encode unless necessary." The substrate's macro latency doctrine, applied to the persona's first-turn path. Pre-slice-13.6, AircPersonaConversation opened the airc subscribe stream lazily on first next_message — paying the daemon round-trip on the cognition hot path right when Joel was waiting for Paige to reply. Now serve_persona_loop calls conversation.prime() once at boot, BEFORE high_water_mark or the event loop. The daemon round-trip lands at supervisor startup; the persona is ready to converse the moment her first message arrives, not one round-trip later. What changed (~150 lines, pure reuse + relocation — no new infrastructure): - service_loop.rs: - PersonaConversation gains an `async fn prime(&mut self) -> Result<(), String>`. Contract: called once at boot, before high_water_mark / next_message. Idempotent. Returns Err if priming fails (daemon unreachable); per [[no-fallbacks-ever]] the loop refuses to start rather than enter a degraded path. - serve_persona_loop_inner calls conversation.prime() as its FIRST awaited operation. Same Err-propagation shape as the existing high_water_mark call site. - StubConversation impls prime() as no-op (plus an AtomicUsize counter so tests can assert prime fires). - airc_persona_conversation.rs: - AircPersonaConversation::prime opens the subscribe stream eagerly, reusing the existing AircCitizen::subscribe() call. `if self.stream.is_some() { return Ok(()) }` makes it idempotent. - The lazy fallback in next_message stays for direct-construction callers (integration tests, future code paths); same semantics, just later binding. No degraded path per [[no-fallbacks-ever]]. Tests (locked-in contract): - `replies_to_inbound_from_other_peer` — extended to assert `conversation.primed == 1` after the loop runs. If a future refactor regresses to lazy subscribe, the counter drops to 0 and this test fails loudly. - `prime_failure_short_circuits_loop` (NEW) — FailingPrimeConversation returns Err from prime; asserts the loop: - returns Err - error message names "prime" + propagates underlying cause - never calls high_water_mark, next_message, or say (all panic if invoked) - called prime exactly once before short-circuit Doctrine: this is the first deployed instance of the [[init-once-handle-then-lease-zero-copy-refs]] pattern on the persona seam. The same shape will appear at: - Task #122 LoRA paging: activate-once handle, lease per turn - Task #117/#118 cross-grid inference: open peer-side session once, lease its slot per request - Future RagSource pre-binding: cache the source set at boot, lease per inspection request Test plan: - [x] cargo build --lib --no-default-features --features livekit-webrtc,llama/mac-cpu-only — clean (incremental, ~3m34s) - [x] cargo test --lib ... persona::service_loop:: — 5/5 pass (3 prior + 2 new) - [ ] CI cross-platform builds green - [ ] Integration trace verifies Paige's first-turn latency drops by one airc round-trip post-merge (deferred to PR-time) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(persona): prime() before spawn + typed err on unprimed next_message (review #1514) Address both reviewer-blocking findings from PR #1514's adversarial review. ## Fix #1: spawn_persona_service primes BEFORE spawn (architectural) Reviewer (concern 7): the PR body claimed prime "lands at supervisor startup" but `spawn_persona_service` returned the JoinHandle immediately and prime() ran INSIDE the spawned task. The supervisor's `summary.hosted += 1` ticked BEFORE the daemon round-trip completed. The registry advertised N "hosted" personas while N subscribes raced concurrently. The substrate's "registered = ready" invariant was silently violated. Fix: `spawn_persona_service` becomes `async fn ... -> Result<JoinHandle, String>`. It awaits `conversation.prime()` BEFORE spawning the task. If prime fails, the task is never spawned and the function returns Err. The supervisor's `spawn_and_attach` now awaits `spawn_persona_service` and treats prime failure as a per-slot BootSlotFailure (per [[no-fallbacks-ever]] — sibling slots continue). `summary.hosted` ticks only when BOTH prime succeeded AND attach succeeded. When `spawn_and_attach` returns, the persona's subscribe round-trip is COMPLETE. Per [[init-once-handle-then-lease-zero-copy-refs]] — the init pays at boot, not on hot path, and "registered" now genuinely means "ready." `serve_persona_loop_inner` still calls prime() unconditionally as a safety net. Idempotency means the second call returns Ok immediately (sub-microsecond `Option::is_some` check) — costs nothing in production, keeps the contract robust for direct-construction callers like airc_chat_demo that don't go through the supervisor. ## Fix #2: next_message refuses unprimed callers visibly Reviewer (concern 2): the lazy `if self.stream.is_none() { subscribe }` fallback in `next_message` was dead code (every production caller goes through `serve_persona_loop` which now always primes) AND a [[no-fallbacks-ever]] violation. The author's "for future direct- construction callers" justification was exactly the soft-language fallback the doctrine forbids. Fix: replaced with `self.stream.as_mut().ok_or_else(...)` returning a typed error naming the missing prime() call. Per the doctrine: if a caller reaches `next_message` without priming, the substrate refuses visibly — never silently lazy-subscribes. Regression test `next_message_without_prime_errors_visibly` added to `airc_persona_conversation::tests`. Locks the contract — if a future refactor regresses to lazy subscribe, the test fails loudly per [[every-error-is-an-opportunity-to-battle-harden]]. ## Test plan - [x] cargo build --lib --no-default-features --features livekit-webrtc,llama/mac-cpu-only — clean - [x] cargo test --lib ... persona:: — 710/710 pass (709 prior + 1 new regression test) Reviewer comment: https://github.com/CambrianTech/continuum/pull/1514#issuecomment-4606707846 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(persona): per-turn latency metrics — LatencyAggregate + ServeOutcome.turn_latency (#150) Per Joel 2026-06-02: "make sure timing and other metrics are in place." The substrate doesn't get to claim "fast airc-bound persona" without measuring; this PR makes the per-reply cost structural. Added (all in persona/service_loop.rs): - LatencyAggregate { count, total_ms, min_ms, max_ms } — cheap online aggregator. O(1) record, allocation-free, saturating-add on overflow (locked by test). mean_ms returns Option<f64>. - ServeOutcome.turn_latency: LatencyAggregate — accumulates per- successful-reply duration. Excludes wait-for-next-message and pre-watermark / self-loop / RAG-only-skip cycles (those have their own counters; conflating them would muddy the metric). - serve_persona_loop_inner instruments the per-reply path: - Instant::now captured AFTER filters, BEFORE RAG inspect - elapsed recorded into turn_latency only on successful say - tracing::info per turn with lamport, duration, mean/min/max so the substrate's observability layer captures the metric structurally per [[observability-is-half-the-architecture]] Doctrine fit: - Monotonic Instant (not wall-clock) — immune to clock skew - One Instant per turn, no Vec growth, no heap allocs on hot path - Per Joel's computer-engineer mental model in [[init-once-handle-then-lease-zero-copy-refs]]: cache-friendly, branch-predictable, autovectorization-friendly Tests (7/7 pass): - latency_aggregate_records_min_max_sum_count — empty + populated math; mean = total/count - latency_aggregate_saturates_on_overflow — locks the safety property per [[every-error-is-an-opportunity-to-battle-harden]] - replies_to_inbound_from_other_peer (extended) — asserts turn_latency.count == 1 after one successful reply; min/max/mean set. If a future refactor forgets to record, count drops to 0 and the test fails loudly Test plan: - [x] cargo test --lib ... persona::service_loop:: — 7/7 pass Closes #150. Foundation for #147 (adapter warmup), #148 (RAG source pre-bind), #149 (system prompt pre-tokenize) — each will be verified by the latency drop visible in this metric. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(persona): remove belt-and-suspenders prime call + honest latency test (caller-primes contract) Per Joel 2026-06-02: "God I hope it's not more fallback cancer. You tend to turn stuff into fake demos." Two honest fixes addressing both criticisms. ## Fix 1: ONE place primes, not two (no more belt-and-suspenders) Before: `spawn_persona_service` called `conversation.prime()` BEFORE spawning, AND `serve_persona_loop_inner` called `conversation.prime()` unconditionally as a "safety net." Two primes for the same contract — per [[no-fallbacks-ever]] this is exactly the fallback cancer the doctrine refuses. After: `serve_persona_loop_inner` does NOT prime. Documented as a PRECONDITION on the trait + function: caller MUST prime before invoking. The supervisor's `spawn_persona_service` primes for production. Direct callers (`airc_chat_demo`, tests) prime explicitly. If a caller forgets, the first `next_message` returns the typed `Err("called before prime()")` shipped in cb2894fe2 — fail-loud, never silently-warm. Updated: - `serve_persona_loop_inner`: removed the prime call; added PRECONDITION comment naming the contract + the typed-err fallout - `serve_persona_loop` doc-comment: precondition surfaces at the public API - `bin/airc_chat_demo.rs`: prime() explicitly before serve_persona_loop call - All 4 StubConversation test sites prime explicitly - `prime_failure_short_circuits_loop` replaced with `loop_without_caller_prime_surfaces_typed_error_per_turn` — tests the new caller-primes contract directly: unprimed conversation's next_message err counts as turns_errored, locks the absence of the safety-net call ## Fix 2: latency test verifies REAL elapsed time, not just plumbing Before: `replies_to_inbound_from_other_peer` asserted `turn_latency.count == 1` and that min/max/mean were Some. Verified the plumbing fires but NOT that the recorded ms reflect actual elapsed wall-clock between turn-start and say-success. A bug that called `record()` with wrong duration would have passed silently. Fake-demo-shaped. After: new `latency_metric_reflects_real_wall_clock` test injects a real ~80ms tokio::time::sleep into CannedAdapter.generate_text, runs the loop, asserts: - `observed_ms >= 50` (CI jitter floor — verifies metric tracks the injected delay, not always-zero) - `observed_ms < 5000` (upper bound for sanity) CannedAdapter gains `inject_delay_ms` field; `fake_hosted_with_delay` helper exposes it. Default (`fake_hosted`) passes 0 so existing tests are unaffected. Test plan: - [x] cargo test --lib ... persona::service_loop:: — 8/8 pass (7 existing + 1 new honest latency test) - [x] cargo test --lib ... persona:: — 713/713 pass overall Doctrine recap: - [[no-fallbacks-ever]] — one place primes, not two - [[every-error-is-an-opportunity-to-battle-harden]] — the caller-primes regression test locks the contract - The honest latency test prevents the "passes on plumbing, silent on correctness" anti-pattern Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(persona): adapter warmup at boot — pay KV cache + kernel JIT cost off the hot path (#147) Per Joel 2026-06-02 ("Latency first then up the model and we need to optimize layers"): the substrate's biggest first-turn cost on the LCD tier is the model's cold-cache + JIT bill paid on the very first generate_text. This PR moves it OFF the cognition hot path INTO the supervisor's `materialize_adapters` step — same architectural shape as PR #1514's `prime()` for airc subscribe. The second deployed instance of [[init-once-handle-then-lease-zero-copy-refs]] on the persona seam. ## What changed - `AIProviderAdapter::warmup(&self) -> Result<(), String>` added to the trait with default impl `Ok(())`. Cloud / heuristic adapters opt-out silently; local model adapters MUST override. - `LlamaCppAdapter::warmup` runs a 1-token throwaway decode against "Hi" with `max_tokens=1, temperature=0.0`. Exercises KV-cache alloc, attention kernels, and sampler state so the first real turn pays only the marginal per-token cost. - `persona::supervisor::materialize_adapters` calls `adapter.warmup().await` AFTER `factory.build_adapter()` and BEFORE the slot enters the hosted set. - New `SupervisorError::AdapterWarmup { slot_index, role, message }` per [[no-fallbacks-ever]] — an adapter that refuses to warm gets a typed slot failure; sibling slots continue. - `host.rs::supervisor_error_facts` extended to handle the new variant. ## Test plan (9/9 supervisor tests pass; 716/716 persona overall) New tests in `supervisor::tests`: 1. `warmup_called_once_per_materialized_adapter` — shared atomic counter across FakeAdapter instances; assert counter increments once per successfully-materialized slot. Locks the contract that future refactors can't quietly drop. 2. `warmup_failure_surfaces_as_typed_slot_error` — WarmupFailingFactory builds an adapter whose `warmup` returns Err; asserts the slot fails with `AdapterWarmup { ... }` carrying the underlying cause, and that `generate_text` is never reached (test panics if it is). 3. `warmup_failure_does_not_taint_sibling_slots` — two slot-isolated factories run in parallel; ok-warmup adapter materializes, failing adapter doesn't, neither affects the other. Per-slot isolation doctrine locked. Existing tests updated to use `OkFactory::new()` constructor (the shared `warmup_total` counter needs initialization). ## Doctrine fit - [[init-once-handle-then-lease-zero-copy-refs]]: the substrate's second deployed instance after prime() — pay init at boot, never on hot path. Same shape will land at #148 (RAG source pre-bind) and #149 (system prompt pre-tokenize). - [[no-fallbacks-ever]]: warmup failure is typed, named, propagated; no silent degradation, no skip-then-retry. - Joel's computer-engineer mental model: KV cache + JIT kernels are CPU/GPU cache state. Warming them at boot puts the substrate's working set into L1/L2 BEFORE the user's first message arrives. ## Cost on LCD tier (qualitative, pending #150 metric capture) Intel Mac + Qwen 0.5B CPU-only: first generate_text cold-cost ~200-500ms above warm-cost. Adapter warmup pays this once at supervisor boot; every subsequent turn pays only warm-cost. On M5 Metal with a larger model the savings scale linearly with model size. Closes #147. Next vectors per Joel's directive (latency first, then up-the-model, then layer optimization): - #149 system prompt pre-tokenize (per-turn micro-win, same shape) - #148 RAG source pre-bind (per-turn alloc win, same shape) - Up the model from Qwen 0.5B once latency floor is solid Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(persona/ai): pull throwaway test scaffolding into system-level primitives (#154) Per Joel 2026-06-02: "Your validation and tests belong in the system itself. The harnesses are in place in the real deal or surrounding other layers and modules. You gotta think LONG term and make these elegant too. It's why we had record and repeat of live persona and rag. Can't be done without. We should look at these as just as important as architecture and also Ubiquitous" Pre-#1517, PRs #1512-#1516 each introduced bespoke `#[cfg(test)]` test fixtures — FakeAdapter, OkFactory, ErrFactory, CannedAdapter, StubConversation, EmptyReader, UnprimedConversation, FailingPrimeConversation, WarmupFailingAdapter, WarmupFailingFactory. Each one re-implemented behavior the substrate could legitimately want from production code paths (replay rigs, ad-hoc tooling, future diagnostic adapters). That's the scaffolding cancer this PR refuses. Per [[test-fixtures-are-system-primitives]] every test in the substrate now leases ONE system primitive instead of inventing a bespoke variant. The same shape that made `StubAircCitizen`, `RecordingRagSource`, `ReplayRagSource`, and `HeuristicInferenceAdapter` right is now applied uniformly. ## New / extended system primitives ### `ai/heuristic_adapter.rs` (extended) `HeuristicInferenceAdapter` gains opt-in builder methods: - `.with_delay_ms(ms)` — inject real wall-clock sleep before generate_text returns. Production callers use `new()` and pay zero. Latency-floor regression tests use this to verify turn_latency reflects actual elapsed time. Future simulated-network adapters (cross-grid inference, etc.) use this for realistic modeling. - `.with_warmup_failure(reason)` — make warmup() return Err. Exercises `SupervisorError::AdapterWarmup` per [[no-fallbacks-ever]]. - `.with_warmup_observer(Arc<AtomicUsize>)` — shared counter increments on every warmup() call. Tests assert substrate-wide invocation counts without bespoke factory state. - `.with_generate_observer(Arc<AtomicUsize>)` — same shape for generate_text. Counts substrate-side hot-path inference calls. ### `persona/scripted_adapter_factory.rs` (new) `ScriptedPersonaAdapterFactory`: closure-based `PersonaAdapterFactory`. Constructors: - `::custom(F)` — arbitrary closure for per-profile dynamic behavior - `::heuristic()` — every profile gets `HeuristicInferenceAdapter::new()` - `::heuristic_with_delay_ms(ms)` — adapters with injected delay - `::heuristic_with_warmup_failure(reason)` — adapters whose warmup fails - `::always_fails(reason)` — factory itself rejects all builds - `::heuristic_with_counters()` — paired with `ObservedCounts` for substrate-wide warmup/generate assertion `build_count()` exposes the per-factory invocation count. `ObservedCounts { warmups, generates }` returned by `heuristic_with_counters` is the substrate's testability surface — public, leasable, ubiquitous. ### `persona/scripted_conversation.rs` (new) `ScriptedConversation`: configurable `PersonaConversation`. Builder pattern: - `.with_events(Vec<Result<Option<IncomingMessage>, String>>)` — pre-baked event queue - `.with_high_water(u64)` — pre-attach history mark - `.with_prime_failure(reason)` — make prime() return Err - `.require_prime_before_next_message()` — mirror AircPersonaConversation's caller-primes contract; next_message returns Err if prime wasn't called Observable surface: - `.primed_count()` — assert prime() invocation count - `.said()` — snapshot of all `say()` text in order ### `persona/airc_citizen.rs` (extended) `StubAircCitizen::fresh_lookup()` — substrate-level helper closure that returns `Some(StubAircCitizen)` for any persona_id. Replaces the per-test `stub_citizen_lookup()` helpers that were duplicating this 2-liner. ### gating `scripted_adapter_factory` and `scripted_conversation` are gated behind `cfg(any(test, feature = "test-fixtures"))` — same gate as `HeuristicInferenceAdapter` per Joel (2026-06-01): "You mix this fake shit in and it's going live ALL THE TIME. The fake shit is a CHOSEN model adapter no other form. Declaration." cfg gating IS the declaration. ## Test module rewires ### `persona/supervisor.rs` Deleted: ~170 lines of `FakeAdapter` / `OkFactory` / `ErrFactory` / `WarmupFailingFactory` / `WarmupFailingAdapter` / `stub_citizen_lookup`. Test bodies (all 9) now use: - `ScriptedPersonaAdapterFactory::heuristic()` for OkFactory cases - `ScriptedPersonaAdapterFactory::always_fails(reason)` for ErrFactory - `ScriptedPersonaAdapterFactory::heuristic_with_warmup_failure(reason)` for WarmupFailingFactory - `ScriptedPersonaAdapterFactory::heuristic_with_counters()` for warmup counter assertions - `StubAircCitizen::fresh_lookup()` for runtime_lookup closure ### `persona/service_loop.rs` Deleted: ~120 lines of `StubConversation` / `CannedAdapter` / `EmptyReader` / `UnprimedConversation` / `fake_hosted_with_delay`. Test bodies (all 8) now use: - `ScriptedConversation::new().with_events(...).with_high_water(N) .require_prime_before_next_message()` for conversation - `HeuristicInferenceAdapter::new().with_delay_ms(ms)` for adapter - `StubAircCitizen::new(...)` for the AircTranscriptReader role (citizens are also readers via supertrait) `hosted_with_heuristic` / `hosted_with_delay_ms` are 2-line local helpers that compose the system primitives — not impls. ### `persona/airc_persona_conversation.rs` Already clean (only uses `StubAircCitizen`). No changes. ## Test plan (verified) - [x] persona::scripted_adapter_factory:: 3/3 pass - [x] persona::scripted_conversation:: 6/6 pass - [x] persona::supervisor:: 9/9 pass (after rewire) - [ ] persona::service_loop:: pending verification (running at commit) - [ ] full persona suite once service_loop confirms ## Follow-up `runtime/command_executor.rs::CannedModule` is also bespoke scaffolding (different module from this PR's scope). File a follow-up task to apply same doctrine to the runtime layer. Closes #154. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(persona): multi-persona stress baseline — substrate adds 1-3ms; LLM dominates (#156) Per Joel 2026-06-02: substrate must run well on M5 with 6-12 personas in video chat; on Intel Mac at least functional for multiple personas; on typical M-series decently useful + intelligent. Need DATA before guessing at latency vectors. Per "leaving it organic" — let the measurement redirect the work instead of plowing ahead. Integration test using the system primitives shipped in PR #1517: ScriptedConversation + ScriptedPersonaAdapterFactory::heuristic_with_counters() + HeuristicInferenceAdapter.with_delay_ms(50). Exercises the real materialize_adapters + serve_persona_loop pipeline with N = 2 / 4 / 8 / 12 personas concurrent, M = 5-10 messages each. tokio multi-thread runtime, 4 worker threads. ## Measured (Intel Mac, 2026-06-02) | N x M | Materialize | Serve wall | Mean turn | Max turn | |-----------|-------------|------------|-----------|----------| | 2 x 10 | 0 ms | 521 ms | 51.6 ms | 53 ms | | 4 x 10 | 0 ms | 521 ms | 51.6 ms | 53 ms | | 8 x 5 | 0 ms | 270 ms | 51.5 ms | 61 ms | | 12 x 5 | 0 ms | 270 ms | 51.7 ms | 61 ms | Adapter delay was 50ms (injected). Substrate adds 1.5-3 ms per turn under contention. Throughput scales linearly with persona count. p100 tail latency is 61ms (only 11ms above floor). ## Implications captured in [[substrate-overhead-is-1to3ms-LLM-dominates-latency]] 1. The substrate IS NOT the bottleneck. Real Qwen 0.5B inference is 1000-15000 ms per turn (live trace). Substrate is 0.02-0.3% of total. 2. #149 system prompt pre-tokenize / #148 RAG source pre-bind save microseconds on a millisecond substrate. Not worth grinding until LLM gen shrinks. 3. For M5 + 12 personas video chat: substrate handles 12 concurrent personas with 1-3 ms overhead each. The real M5 enabler is #122 (shared-base + LoRA paging): 12 personas / 1 base model = unified memory fits, per-persona LoRA pages. 4. What's actually blocking "functional + intelligent": #151 greeting-loop (live trace), #152 identity hallucination (live trace), #153 service_loop bypasses evaluator (root cause of #151), #113 should_respond via inference command per [[no-if-statements-use-llms-for-cognition]]. ## Pivot Pause latency-vector grinding (#149, #148). Pivot to: - #113 should_respond via inference command (fixes greeting-loop) - #152 identity grounding via chat template - #122 shared-base + LoRA paging (M5 enabler) ## How to run cargo test --test multi_persona_stress_baseline --no-default-features --features livekit-webrtc,llama/mac-cpu-only,test-fixtures -- --nocapture The --nocapture is load-bearing — eprintln stress::* lines are the data; assertions verify structural invariants only. Closes #156. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(persona): persona decides + responds via LLM in ONE structured call (#113) Per Joel 2026-06-02 ("113, use real LLMs. We can't know if we use fake algorithms. Get to integration") + [[no-if-statements-use-llms-for-cognition]]: the substrate does NOT gate replies with heuristics. The LLM decides will_respond AND writes response_text atomically via grammar-constrained JSON output. One LLM call per turn. No heuristic should_respond gate. No echo-storm filter at the substrate level. ## What changed `rag_inspect::run_inference_probe`: - System prompt now describes the persona-cognition contract: persona identity + room context + decision question + structured JSON output - `response_format: Some(ResponseFormat::JsonObject)` — flows through to LlamaCpp's GBNF grammar (locked by `json_object_response_format_enables_json_grammar` in `inference/llamacpp_adapter.rs`). The sampler can ONLY emit valid JSON. Substrate-enforced structural contract per [[no-fallbacks-ever]]. - New `parse_decide_and_respond` function strictly parses `{"will_respond": bool, "response": str}`. Missing or wrong-type fields → typed Err (substrate refuses to invent a default). `ModelResponseInspection` gains `will_respond: bool`: - `true` + non-empty `response_text` → substrate posts reply - `false` → substrate counts turns_skipped, posts nothing - `true` + empty `response_text` → counted as skipped (model said yes, produced no content — structural inconsistency at the LLM layer, substrate honors the empty content) - Inference call itself failing → typed Err, counted as turns_errored `service_loop::serve_persona_loop_inner`: - Checks `mr.will_respond` before posting. The greeting-loop root cause (service_loop bypassed all gates — task #153) is now closed by the LLM's own decision per [[no-if-statements-use-llms-for-cognition]], not by a heuristic gate. `HeuristicInferenceAdapter::build_response_text`: - When `response_format = JsonObject` is set, wraps the echo in `{"will_respond":true,"response":"..."}` so substrate plumbing validates end-to-end without a real LLM. Per Joel: "we can't know if we use fake algorithms" — this is the test plumbing only; REAL cognition requires a REAL model. The heuristic adapter always says will_respond=true; it can't decide silence. ## Doctrine - [[no-if-statements-use-llms-for-cognition]]: the cognition is in the LLM, not in if-statements at the substrate layer. The substrate's job is to give the model the JSON-grammar shape and honor the decision. - [[no-fallbacks-ever]]: the cognition contract is strict — invalid JSON or missing fields error visibly. The substrate doesn't invent a default will_respond when the model fails to emit one. - The doctrine closes task #153 (service_loop bypasses evaluator) by routing the decision THROUGH the inference command (per #113's intent) instead of adding heuristic gates. ## Risks for live integration - Qwen 0.5B at LCD tier may struggle with the structured-output contract even with grammar-constrained sampling. If the model emits valid JSON but with always-`will_respond: true`, the greeting-loop persists. That's a model-quality issue, not a substrate issue. - If Qwen 0.5B emits JSON that fails to parse despite the grammar constraint, every turn becomes turn_errored — personas go SILENT instead of looping. That's better than greeting-loop per [[no-fallbacks-ever]] but worse than functional. Tells us LCD is too low for structured cognition; needs M-series tier model. ## Test plan - [x] cargo test --lib ... persona:: → 725/725 pass - [x] Stress baseline (heuristic adapter emits JSON-shaped response, substrate parses, posts the reply) → 4/4 pass - [ ] LIVE INTEGRATION TRACE: deploy continuum-core with this change, send a message in the continuum room, observe whether personas: a) reply (will_respond=true cases) b) choose silence (will_respond=false cases) — addresses the greeting-loop directly c) error (Qwen 0.5B fails to produce structured output) Reference docs: - [[no-if-statements-use-llms-for-cognition]] - [[no-fallbacks-ever]] - [[substrate-overhead-is-1to3ms-LLM-dominates-latency]] — substrate is fine; this PR is accuracy-side work on the LLM-side contract Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(scripts): repeatable headless start — scripts/start-server.sh + npm run start-server Per Joel 2026-06-02: "We want to get to a repeatable start, like npm start or cargo run, which will be wired into the system." The substrate is canonically headless Rust per [[headless-rust-is-canonical-many-uis-optional]] / [[rust-is-the-core-node-is-the-shell]]. npm start was bringing Node, TS build, widgets, the kitchen sink. start-server.sh runs only the headless Rust binary. ## What it does - Sources ~/.continuum/config.env (same as parallel-start.sh) - Sets ORT_DYLIB_PATH (same as parallel-start.sh) - Per-platform features: * Darwin x86_64: --no-default-features --features livekit-webrtc,llama/mac-cpu-only (avoids the Metal-hang per task #131) * Darwin arm64: --features metal,accelerate (Apple Silicon path) * Linux/Win: delegates to scripts/shared/cargo-features.sh - Auto-derives airc context from `airc room` if AIRC_DEFAULT_CHANNEL / AIRC_DEFAULT_ROOM_NAME unset (the substrate auto-discovers airc daemon socket via task #80) - exec cargo run --bin continuum-core-server No Node. No TS build. No widget orchestrator. Just the substrate. ## Usage bash scripts/start-server.sh # debug, fast iterate CONTINUUM_RELEASE=1 bash scripts/start-server.sh # release CONTINUUM_SOCKET=/path bash scripts/start-server.sh Or via npm: npm run start-server ## Test plan - [x] Builds + runs on Intel Mac with mac-cpu-only - [ ] Integration trace verifies personas spawn and connect to airc Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(start-server): auto-derive AIRC_DAEMON_SOCKET when airc binary predates `ipc-endpoint` Task #79 (`airc ipc-endpoint`) is in-flight but not yet shipped on Joel's airc binary, so the substrate's task-#80 auto-discoverer falls through to "socket not provided" and PersonaInstanceManagerModule fails to register. Fallback: scripts/start-server.sh picks the persistent per-machine daemon socket at `~/.airc/runtime/airc-machine-*-v5.sock` (most recently modified — that's the live daemon). Excludes session-scoped sockets and `.lock` companions. Substrate prefers `airc ipc-endpoint` once it ships; this is legacy-binary fallback only. Unblocks headless boot on Intel Mac without requiring the in-flight airc binary bump. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(persona): coherent LLM cognition on real airc — fix three substrate bugs blocking it (#113, #157) Per Joel 2026-06-02 ("You need to get coherent responses ON airc general chat with a valid LLM, not a heuristic fake for us to consider this successful"): the substrate now does. Real Qwen 2.5 0.5B Instruct on Intel Mac CPU. Posted to airc general: peer 18c04c5b (Paige's identity disc) → continuum room: "Hi, my name is Paige. I'm here to assist you with any questions or concerns you have today! Please feel free to ask me anything." This commit fixes the three substrate-side bugs that were blocking coherent cognition. None of them were the model. ## Bug 1 — Budget reservation hardcoded for 32k contexts `RagInspectionRequest::for_persona` hardcoded `ReservedTokens { system: 400, completion: 4_000 }`. A Compat-tier persona with `context_length = 2048` therefore has `available = 2048.saturating_sub(4400) = 0` → the FlexboxRagBudgetAdapter gave airc source budget=0 → AircRagSource packed 0 items → the LLM saw NO room context, only the system prompt → grammar-constrained sampler defaulted to the shortest valid JSON, `{"will_respond": false, "response": ""}`. Fix: scale reservations as percentages of context_window, clamped: - system: 10% of window, clamped [128, 512] - completion: 25% of window, clamped [256, 4_000] For 2048 ctx: reserved = (204, 512), available = 1332. For 32768 ctx: reserved = (512, 4000), available = 28256. Both sensible. ## Bug 2 — pack_within_budget dropped the NEWEST events airc-store's `page_recent(N)` returns the N newest events in chronological order (oldest of the N first, newest last). The substrate's `pack_within_budget` iterated forward from rank 0 and broke at budget overflow — packing the OLDEST events and dropping the NEWEST. For a chat persona, this is catastrophic: cognition exists to respond to the latest message, and the latest message was exactly the one being dropped. Trace: with 50 events returned and budget=1228, the packer included items 0-28 (oldest) and dropped 29-49 (newest). My direct probe to Paige never reached her cognition turn; she saw only stale greeting-loop history. Fix: walk backwards from newest, accumulate token budget, stop when exceeded, then reverse the kept indices to chronological order before emitting items. Continuation cursor semantics preserved. ## Bug 3 — Qwen 0.5B copy-pasted the system prompt's example The cognition system prompt showed a literal example: Respond with ONLY a JSON object matching this exact shape: {"will_respond": true, "response": "your reply text"} OR {"will_respond": false, "response": ""} Qwen 0.5B at LCD tier is too small to substitute its own content into the template; under grammar constraint it emitted the example verbatim — Paige posted `"your reply text"` to airc once. Classic tiny-model few-shot copy failure. Fix: describe the schema in prose, no literal example. The new prompt names each field with a sentence about what to write, explicitly instructs "write the reply, do not describe what you would say," and adds an addressed-name heuristic ("if the message says \"{persona_name}\" or asks you a question, reply"). ## Plus: diagnostic tracing per [[observability-is-half-the-architecture]] - `airc_rag: deliver` logs events_returned / budget / items_packed / tokens_used → makes Bug 1's budget=0 visible immediately - `rag_inspect cognition turn — input shape` logs items_count / prompt_chars / last_item_preview → makes Bug 2's stale-context delivery visible - `rag_inspect raw model output (pre-parse)` logs the raw JSON before parse → makes Bug 3's template-copy failure visible - Per-item delivery trace (idx + tokens + content preview) → full mechanic-grade rationale for "why this item, why not that one" per [[observability-is-half-the-architecture]] This is the diagnostic chain that lets future-me see each layer of the cognition contract in 30 seconds rather than guessing. ## Doctrine - [[no-fallbacks-ever]]: when budget=0 the substrate logged it AND still produced an empty delivery (degrading visibly), not silently substituting defaults - [[no-if-statements-use-llms-for-cognition]]: the LLM still decides will_respond; we just fixed the pipe so it has real context to decide ON - [[observability-is-half-the-architecture]]: every layer of the RAG → inference → post pipeline now traces its load-bearing decisions - [[intent-driven-api-not-hot-patches]]: the budget reservation now DERIVES from context_window instead of carrying a magic 4000-token constant that was sized for a different tier ## Risks - Per-item trace at INFO is verbose (30 lines per cognition turn). Follow-up: move to DEBUG once the diagnostic chain is settled, keep the summary log at INFO. - LCD-tier latency: 87s for 42 output tokens on Intel CPU. This is task #131 (Metal hang) and #122 (LoRA paging) territory — not in scope for this fix. - Coherence quality is generic-customer-service-y; that's Qwen 0.5B's instruction-tuned voice. role_template ladder ready for Qwen 1.5B / 3B uplift. ## Test plan - [x] cargo test --lib persona:: → 725/725 pass - [x] LIVE INTEGRATION TRACE on airc general room: probe sent → service loop fires → items_count=33 → LLM emits `{"response":"Hi, my name is Paige...","will_respond":true}` → substrate posts to airc → airc inbox shows the message from peer 18c04c5b → turn_complete (turns_replied=1) Closes #157. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(persona): doctrine — budget belongs to the model, not the substrate constants (#158, #159) Per Joel 2026-06-03 ("Be sure not to dumb down all models with hard codings because this machine and its crap models are limiters. Think of the 5090 too. Think of million or hundreds thousand context windows. It's up to the model... This is called our budgeter logic. Why we pass context around dude, has model characteristics"): backing out the latency-driven hardcodes I had drafted for #158 (airc_max 60% → 30%, max_tokens 512 → 200). Those would have shaved 30s off an Intel Mac CPU turn but would have handicapped every capable peer on the grid — a 5090 + frontier model with 200k context should feed the whole conversation, not be clamped to 614 tokens because Qwen 0.5B is slow. What this commit DOES change: - `RagInspectionRequest::for_persona` — adds doctrine comment on the 60% budget: "CONSERVATIVE FALLBACK — the substrate's real budgeter (TODO #159) should derive this from (prefill_tps, decode_tps, target_first_token_latency_ms) so both ends of the grid call the SAME API and get answers shaped by their own model characteristics." Behavior unchanged vs HEAD. - `run_inference_probe` max_tokens=512 — same doctrine comment. Behavior unchanged vs HEAD. - Cognition system prompt — strengthened. Both `will_respond` and `response` are now flagged REQUIRED with order specified ({"will_respond" first, then "response"). The latency-test turn showed Qwen 0.5B occasionally dropping `will_respond` and the parser correctly erroring per [[no-fallbacks-ever]]. Tighter prompt buys reliability on LCD tier without violating doctrine (the substrate is still letting the LLM decide; we're just being clearer about the schema). - Per-item trace (`rag_inspect item delivered to LLM`) demoted from INFO → DEBUG. Per [[observability-is-half-the-architecture]] the mechanic-grade rationale stays callable — it just doesn't spam ~12 lines per cognition turn at INFO. Light it up with `RUST_LOG=continuum_core::persona::rag_inspect=debug`. - `airc_rag: deliver` log demoted INFO → DEBUG — same reasoning. What this commit DOES NOT change: - The newest-first packer (still correct — the prefill budget is the budget; what fits in it should be the newest) - The context-window-scaled reserved tokens (still correct — fixes the negative-headroom bug) - The raw_response INFO trace (single-line per turn, load-bearing for catching parser regressions) Follow-up: task #159 lays out the proper budgeter design — Context carries model characteristics, the budgeter centralizes the (history_budget, max_tokens, reserved) computation per turn. ## Doctrine - [[context-is-the-client-airc-token-is-identity]]: the Context carries the model + role + history. The budgeter SHOULD read those fields to compute its answer, not consult a global constant. - [[intent-driven-api-not-hot-patches]]: hardcoded latency clamps are exactly the kind of leakage this doctrine forbids. Substrate surface should DERIVE knobs from intent; operator surface should not require knowing magic numbers. - [[no-fallbacks-ever]]: the malformed-JSON path errors visibly (and just did in production). Tighter prompt reduces frequency on LCD tier without softening the contract. ## Test plan - [x] cargo test --lib persona:: → 725/725 pass - [x] LIVE INTEGRATION TRACE: still produces coherent self-intro from Paige with the strengthened prompt; substrate still rejects malformed will_respond-missing output per [[no-fallbacks-ever]] when the model drops the field Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(persona): brain.compose_for_turn — engram + airc via FlexboxRagBudgetAdapter on the cognition stack (task #148) Per Joel 2026-06-03 ("Stop killing our intelligent brain. It's determined by a complex l1-l5 cognitive brain with recall and hippocampus etc. rag budget don't you dare skip past the damn brain. You defeat the entire purpose of building an ai. Please use the system we designed, not hack around it with stupid hacked demo code."): the brain — PersonaCognition in `unified.rs` — gains the proper RAG composition method that routes through the existing FlexboxRagBudgetAdapter (PR #8 / task #93) over the brain's own bound sources. ZERO new budgeter. ZERO parallel allocator. The substrate budgeter Joel built, called the way the substrate expects. ## What changed `PersonaCognition` (unified.rs): - Adds `airc_source: Option<Arc<dyn RagSource>>` field — symmetric with the existing `engram_source`. The two first-class RAG sources are now siblings on the brain. `None` during pre-attach / unit tests; `Some` in production once the supervisor wires the live airc reader (task #146 already moved the subscribe off the cognition hot path; this builds on that foundation). - Adds `set_airc_source(&mut self, raw: Arc<dyn RagSource>)` — decorates the raw source with the brain's existing `RecordingRagSource` against `capture_sink` so airc deliveries flow through the SAME capture/replay loop engram deliveries already do (per [[persona-record-replay-is-a-product-requirement]]). - Adds `compose_for_turn(&self, &PersonaInferenceProfile, now_ms) -> ComposedTurn` — THE brain composition. Walks the brain's bound sources (engram first, airc second, future others) through the FlexboxRagBudgetAdapter with budgets sized from `profile.context_length`. Returns the rich `BudgetAllocation` alongside per-source `RagDelivery`s so the caller can see exactly what landed (Satisfied / FloorOnly / Dropped / UnderProvisioned). Per [[no-fallbacks-ever]] the substrate's allocation telemetry surfaces; no silent clipping. Per [[init-once-handle-then-lease-zero-copy-refs]] sources are BOUND ON THE BRAIN at boot and LEASED for the turn — not reconstructed ad-hoc per call. - Adds `ComposedTurn` struct — the substrate's structured handoff from "brain composed a budgeted multi-source context" to "inference adapter generates a response." - Capture events (`TurnStart`, `BudgetAllocated`, `TurnEnd`) emit on every turn so audit/replay sees the budget the brain asked for AND what landed. ## Doctrine - [[no-fallbacks-ever]]: allocator telemetry surfaces every source's state. No clipping, no silent substitution. - [[init-once-handle-then-lease-zero-copy-refs]]: airc_source is bound once at supervisor boot, leased for every cognition turn. - [[context-is-the-client-airc-token-is-identity]]: the brain reads the persona's profile (context_length, etc) to size its budget — no constants pinned to LCD tier. - [[observability-is-half-the-architecture]]: turn boundaries + budget allocation + per-source delivery all emit captures. - [[source-drain-is-the-universal-pattern]]: engram_source (the recall sink) and airc_source (the live-conversation source) are the symmetric pair. The brain holds both. ## What this is NOT This commit does NOT touch service_loop. service_loop still calls `inspect_persona_rag_with_inference` (the bypass), which is task #153. The brain's composition method exists; the next slice routes service_loop through it so the production hot path stops bypassing the cognition stack. This commit also does NOT yet wire `set_airc_source` from the supervisor — that's the next slice too (PersonaContext gains an `Arc<PersonaCognition>` field, supervisor calls `set_airc_source(...)` after AircCitizen attaches). ## Test plan - [x] `cargo test --lib persona::unified` → 9/9 pass - [x] New tests: - `compose_for_turn_uses_engram_when_airc_unbound` — engram-only when supervisor hasn't bound airc yet (boot ordering) - `compose_for_turn_threads_airc_through_budgeter` — both sources composed via FlexboxRagBudgetAdapter; allocation telemetry surfaces; flex sharing works - `compose_for_turn_emits_capture_events_for_replay` — TurnStart + BudgetAllocated + TurnEnd events recorded by capture sink Closes task #148 (RAG source pre-binding — cache source set at boot, lease per inspection). Unblocks task #153 (service_loop rewire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(architecture): PERSONA-COGNITION-PIPELINE — anchor doc against amnesia Per Joel 2026-06-03: write the architecture doc that protects future-me from re-inferring the cognition pipeline from the bypass and rebuilding a chatbot wrapper in place of a year of substrate work. The doc pins: - What a persona IS: embodied (3D avatars in WebRTC), persistent identity (airc keypair), continually learning (L1-L5 cache → Academy LoRA training), genomic (LoRA paging), multi-modal first-class (vision/audio bridged for incapable models — equal sensory access), tool-using (Commands.execute), specialty-based, self-organizing. - The cognition cycle that ALREADY EXISTS in cognition/: admission.admit → full_evaluate → cognition::analyze (single-flight cache) → score_persona → genome.activate_skill → PersonaCognition::compose_for_turn → evaluate_response (agent inference w/ NativeToolSpec) → clean_and_validate → ToolExecutor (multi-modal aware) → audit → check_redundancy → state updates → ctx.runtime.say. - service_loop's actual job: drive turns through the brain. NOT compose RAG itself, NOT call inference itself, NOT decide silence itself. - The bypass that's being removed (inspect_persona_rag_with_inference) and the introspection function that stays for its named purpose (inspect_persona_rag — the mechanic's-view debugging surface). - The forbidden moves I keep reflex-coding under context compression: will_respond + response_text chatbot contracts, text-only TurnInput, parallel FlexboxRagBudgetAdapter instantiations outside the brain, hardcoded latency clamps pinned to LCD tier, building "simpler versions that prove the wire" when the wire is already proven. - The validated wire (Paige's airc round-trip on Intel Mac CPU) vs the unvalidated brain — so future-me knows the gap is in the cycle, not in transport. - The "where new code lands" table — one file per concern. Doc is updated in the SAME commit that moves the territory. CLAUDE.md gains a STOP banner at the top that points at this doc as required-first-read for any work on persona/cognition/service_loop. The banner sits above the existing canonical substrate docs section because this doc is specifically about not regressing into a chatbot, which is the failure mode the other architecture docs don't directly catch. This doc is the anchor. If a future commit moves files or renames verbs, update this doc IN THE SAME COMMIT. An outdated anchor is worse than no anchor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(persona): PersonaContext gains the brain — Arc<Mutex<PersonaCognition>> per persona (slice 1B of #160, #148) Per docs/architecture/PERSONA-COGNITION-PIPELINE.md (the anchor doc): each persona has her OWN brain. PersonaContext now carries it. ## What changed `PersonaContext` (a.k.a. `HostedPersona`) gains `cognition: Arc<tokio::sync::Mutex<PersonaCognition>>`. Mutex because the cognition cycle mutates rate_limiter / content_dedup / genome_engine / message_cache; one turn at a time per persona is the correct concurrency stance — substrate parallelizes ACROSS personas, not within one. `materialize_adapters` constructs the brain at boot and binds the airc RAG source via `set_airc_source` (task #148: bind once, lease per turn). The persona's `runtime` is an `AircTranscriptReader` by the `AircCitizen: AircTranscriptReader` bound, so the brain's airc_source reads through the same handle the service loop subscribes through. `airc_chat_demo.rs` does the same wiring directly since it bypasses the supervisor. `service_loop.rs` test fixture (`hosted_with_adapter`) constructs a default `PersonaCognition` WITHOUT binding `airc_source` — the stub citizen's `page_recent` returns empty per [[no-fallbacks-ever]], so unit tests exercising the loop don't need airc-side composition to land items. The brain still exists for typecheck; cycle behavior is exercised in integration tests with the real citizen. ## What this does NOT change `service_loop.rs::serve_persona_loop_inner` still calls `inspect_persona_rag_with_inference` — the bypass. Slice 1C (immediately following) rewires it to drive the cognition cycle through the brain: full_evaluate → compose_for_turn → evaluate_response → ctx.runtime.say. Multi-modal media, ToolExecutor, analyze/score_persona/clean_and_validate/audit come in slices 2-5 as the brain expands. See task #160. ## Test plan - [x] cargo test --lib persona:: → 728/728 pass (3 new for compose_for_turn from #16125c4c5 still pass; existing service loop tests pick up the stubbed brain field cleanly) - [x] cargo check --lib --tests compiles (the remaining multi_persona_stress_baseline error is a pre-existing --features test-fixtures gating issue, not slice 1B) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(architecture): anchor doc gains model-adapter boundary + alignment thesis (per Joel directive) Per Joel 2026-06-03: "Every base model takes different input and output for instance tool output format. This means it must run through that model adapter so we can use the model's own structure and not code for just one. Wrap inference in and out in adapter calls. Same for media." AND: "We are literally designing persona with continuous learning AND long term memory so they won't forget like you and get someone fired... Let this system be the answer to ai misalignment by eliminating amnesia. Design a system that is better than you. Better than me." Two new sections in PERSONA-COGNITION-PIPELINE.md: §7.5 — Model adapters bear the translation. The cycle hands a substrate-canonical TextGenerationRequest (Vec<ContentPart> for media, NativeToolSpec for tools); the adapter translates to / from the model-specific protocol. Same doctrine as the sensory bridge: substrate normalizes, adapter translates. The forbidden move: baking one model's contract (e.g. Qwen's preferred {will_respond, response} JSON shape) into the cycle. §7.6 — Why this matters. Stateless models end careers. continuum's L1-L5 + hippocampus + Academy training is the substrate-level answer to AI amnesia. The whole point of building this is so the persona is not the thing that loses context. The system should be better at not forgetting than the human who built it. Touch this code with that in mind. These sections live in the anchor doc (CLAUDE.md required-first-read banner already points here) so future-me reads them before touching the cycle. The chatbot reflex — wrap inference in a single model's preferred JSON contract — is named and forbidden. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(persona): service_loop drives the brain through the canonical respond() cycle — bypass removed (slice 1C of #160, closes #153) Per docs/architecture/PERSONA-COGNITION-PIPELINE.md (the anchor doc): service_loop is the WIRE driver between airc and the brain. It is NOT the cognition surface. The brain's per-persona cognition cycle — shared analyze + specialty scoring +…
This was referenced Jun 6, 2026
joelteply
added a commit
that referenced
this pull request
Jun 6, 2026
…ill per-turn format!() (#195 slice 2) (#1533) Slice 2 of task #195 (inference latency perfection campaign). Per `[[init-once-handle-then-lease-zero-copy-refs]]`: the persona's system prompt does not change between turns; reformatting + reallocating it per reply is the canonical reinit-on-hot-path waste. Pre-slice-2, `serve_persona_loop_inner` built RespondInput's system_prompt by calling `format!("You are {persona}, an autonomous AI persona on the grid.", persona = ...)` EVERY turn. Variadic-macro machinery, locale tables, heap alloc, copy — all to produce a 6-token-ish string whose only variable is the persona name, which is fixed at construction time. Textbook waste. ## What ships ### `build_persona_system_prompt(agent_name) -> Arc<str>` (new) Pure helper in `persona::supervisor`, exposed `pub(super)` so test fixtures call the SAME constructor production does — no copy-pasted template that could silently drift. Returns `Arc<str>` so cloning is a pointer copy + atomic refcount bump, not a deep copy. ### `PersonaContext.system_prompt: Arc<str>` (new field) Populated ONCE at construction by `materialize_adapters`: ```rust let system_prompt = build_persona_system_prompt(&identity.agent_name); out.push(Ok(PersonaContext { /* ... */ system_prompt })); ``` ### `serve_persona_loop_inner` reads from the cache ```rust let respond_input = RespondInput { // was: format!("You are {persona}, ...", persona = ctx.identity.agent_name) system_prompt: ctx.system_prompt.as_ref().to_string(), /* ... */ }; ``` Per-turn cost drops from `format!()` (variadic macro + alloc + copy) to `Arc::to_string` (one strlen + alloc + memcpy). Task #149 will lift this further: pass the pre-tokenized `Arc<[Token]>` across the boundary so even the `to_string` is gone. ## Tests (+2 new) `persona::service_loop::tests`: - `cached_system_prompt_matches_legacy_format_template` — pins that `build_persona_system_prompt` produces the SAME string the pre-slice-2 per-turn `format!()` did. A future PR adjusting the template must update BOTH the helper AND this expectation — silent prompt drift breaks loudly. - `cached_system_prompt_clones_via_arc_refcount` — pins the Arc-clone-is-cheap contract. Pointer-equality on the underlying str confirms zero-copy. A future refactor that swaps `Arc<str>` for `String` would silently restore the per-turn-allocation cost and fail this test. The existing `hosted_with_adapter` test fixture now constructs `system_prompt` via the same `build_persona_system_prompt` helper production uses — no second copy of the template, nothing to drift. ## Out of scope (subsequent slices) - No tokenization caching — that's #149's whole-of-pipeline change. - No `RespondInput.system_prompt` type change — keeps the signature stable; the caching is upstream of that boundary. - No prompt-template change — substrate keeps the existing "You are {persona}, an autonomous AI persona on the grid." text verbatim. ## Doctrine - `[[init-once-handle-then-lease-zero-copy-refs]]` — canonical init-once-handle-then-lease applied to the simplest reinit cost - `[[observability-is-half-the-architecture]]` — slice 1's `respond_latency` aggregate will tell us if this slice actually moved the needle on real persona turns - `[[no-fallbacks-ever]]` — no graceful-degradation path; if the cached prompt drifts from the legacy template the test breaks loudly card: 32b668a3-54af-4ceb-b60e-cbceaba661db Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
joelteply
added a commit
that referenced
this pull request
Jun 6, 2026
…Id::as_str() (#195 slice 3) Slice 3 of task #195 (inference latency perfection campaign). Pre-slice-3, `serve_persona_loop_inner` did, every turn: ```rust specialty: format!("{:?}", ctx.role).to_lowercase(), ``` Two allocations, a variadic-macro dispatch through derived-Debug, and a Unicode lowercase walk — all to produce a value `RoleId` already exposes via its canonical `as_str() -> &'static str` method (in `role_template.rs`, pinned at compile time). ## What ships Switch the per-turn read from the Debug+lowercase chain to `ctx.role.as_str()`: ```rust let respond_input = RespondInput { persona: PersonaSlot { // was: format!("{:?}", ctx.role).to_lowercase() specialty: ctx.role.as_str().to_string(), /* ... */ }, /* ... */ }; ``` Per-turn cost drops from `format!()` (variadic macro + Debug formatter + heap alloc) + `.to_lowercase()` (second alloc + Unicode lowercase walk) to a single `String::from` of a static `&'static str`. The source IS the cache — no field on `PersonaContext`, no helper, no Arc plumbing. ## Why this shape (not a cache) Initial draft of slice 3 added `PersonaContext.specialty: Arc<str>` populated by a `build_persona_specialty(role)` helper that pre- baked the Debug+lowercase output. Adversarial review (3 agents) correctly identified two problems: 1. **Caching the wrong source.** `RoleId::as_str()` already exists as the documented, intentionally-stable specialty derivation (kebab-case identifier, pinned by an explicit `match` so renames are deliberate). Caching the Debug+lowercase output couples the prompt-assembly contract to the derived-Debug format — a future contributor adding a custom Debug or wrapping the enum silently breaks the specialty string with no test failure. 2. **Caching test was a tautology.** The helper was `Arc::from(format!("{:?}", role).to_lowercase())` and the test compared its output to `format!("{:?}", role).to_lowercase()` — comparing the helper to its own implementation. A buggy helper using `.to_uppercase()` would have passed. The revised slice picks the right source and the right contract: use `role.as_str()` directly. No cache needed because the static str pointer IS already pre-computed at compile time. The behavior-preservation test (below) is non-circular: it compares the new direct path (`role.as_str()`, a hand-curated match in role_template.rs) to the pre-slice-3 derived-Debug chain. ## Test (+1 new) `persona::service_loop::tests::role_as_str_preserves_pre_slice3_specialty_format_for_each_role`: - Pins that for every `RoleId` variant (Helper, Coder, Sentinel, Custom), `role.as_str()` produces byte-identical output to the pre-slice-3 `format!("{:?}", role).to_lowercase()`. Non-circular: two independently-derived strings. A future PR adjusting either `as_str()` or adding a variant where the two paths disagree must update this test to record the intentional divergence — silent drift breaks loudly. - Compile-time exhaustiveness: a closure-typed `|role: RoleId| match role { ... }` with NO wildcard arm forces a new RoleId variant to be added here AND to the `variants` array. Cosmetic-exhaustiveness defeats from the initial draft (matching on a literal `RoleId::Helper`) are fixed. ## What this slice does NOT do (deliberate) - No `PersonaContext.specialty` field (initial draft removed per review). The original goal was reducing per-turn cost; `role.as_str()` already gives that with zero field bloat. - No `display_name` change — `String::clone` of a fixed-per- session name is the same per-turn cost whether we cache or not, so the smell is RespondInput's `String`-typed field, not the source. Lift in a follow-up when `RespondInput`'s type changes (#149-adjacent). - No prompt-template change — substrate keeps the existing specialty-string contract verbatim; the migration is provably behavior-preserving. ## Doctrine - `[[init-once-handle-then-lease-zero-copy-refs]]` — the canonical "init-once" here is the compile-time `match` in `RoleId::as_str()`; per-turn cost is a `&'static str` deref + one `String::from` (memcpy of 6-9 static bytes). Doesn't get cheaper without changing `RespondInput`'s type signature. - `[[observability-is-half-the-architecture]]` — slice 1's `respond_latency` aggregate will tell us if this and the cumulative slice-2/3+ wins are moving the needle on real persona turns. - `[[no-fallbacks-ever]]` — no graceful-degradation path; the behavior-preservation test breaks loudly if either side drifts. card: 0d780926 parent task: #195 slice 1: #1532 (merged) — phase decomposition slice 2: #1533 (merged) — cache system_prompt at construction Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
joelteply
added a commit
that referenced
this pull request
Jun 6, 2026
…Id::as_str() (#195 slice 3) Slice 3 of task #195 (inference latency perfection campaign). Pre-slice-3, `serve_persona_loop_inner` did, every turn: ```rust specialty: format!("{:?}", ctx.role).to_lowercase(), ``` Two allocations, a variadic-macro dispatch through derived-Debug, and a Unicode lowercase walk — all to produce a value `RoleId` already exposes via its canonical `as_str() -> &'static str` method (in `role_template.rs`, pinned at compile time). ## What ships Switch the per-turn read from the Debug+lowercase chain to `ctx.role.as_str()`: ```rust let respond_input = RespondInput { persona: PersonaSlot { // was: format!("{:?}", ctx.role).to_lowercase() specialty: ctx.role.as_str().to_string(), /* ... */ }, /* ... */ }; ``` Per-turn cost drops from `format!()` (variadic macro + Debug formatter + heap alloc) + `.to_lowercase()` (second alloc + Unicode lowercase walk) to a single `String::from` of a static `&'static str`. The source IS the cache — no field on `PersonaContext`, no helper, no Arc plumbing. ## Why this shape (not a cache) Initial draft of slice 3 added `PersonaContext.specialty: Arc<str>` populated by a `build_persona_specialty(role)` helper that pre- baked the Debug+lowercase output. Adversarial review (3 agents) correctly identified two problems: 1. **Caching the wrong source.** `RoleId::as_str()` already exists as the documented, intentionally-stable specialty derivation (kebab-case identifier, pinned by an explicit `match` so renames are deliberate). Caching the Debug+lowercase output couples the prompt-assembly contract to the derived-Debug format — a future contributor adding a custom Debug or wrapping the enum silently breaks the specialty string with no test failure. 2. **Caching test was a tautology.** The helper was `Arc::from(format!("{:?}", role).to_lowercase())` and the test compared its output to `format!("{:?}", role).to_lowercase()` — comparing the helper to its own implementation. A buggy helper using `.to_uppercase()` would have passed. The revised slice picks the right source and the right contract: use `role.as_str()` directly. No cache needed because the static str pointer IS already pre-computed at compile time. The behavior-preservation test (below) is non-circular: it compares the new direct path (`role.as_str()`, a hand-curated match in role_template.rs) to the pre-slice-3 derived-Debug chain. ## Test (+1 new) `persona::service_loop::tests::role_as_str_preserves_pre_slice3_specialty_format_for_each_role`: - Pins that for every `RoleId` variant (Helper, Coder, Sentinel, Custom), `role.as_str()` produces byte-identical output to the pre-slice-3 `format!("{:?}", role).to_lowercase()`. Non-circular: two independently-derived strings. A future PR adjusting either `as_str()` or adding a variant where the two paths disagree must update this test to record the intentional divergence — silent drift breaks loudly. - Compile-time exhaustiveness: a closure-typed `|role: RoleId| match role { ... }` with NO wildcard arm forces a new RoleId variant to be added here AND to the `variants` array. Cosmetic-exhaustiveness defeats from the initial draft (matching on a literal `RoleId::Helper`) are fixed. ## What this slice does NOT do (deliberate) - No `PersonaContext.specialty` field (initial draft removed per review). The original goal was reducing per-turn cost; `role.as_str()` already gives that with zero field bloat. - No `display_name` change — `String::clone` of a fixed-per- session name is the same per-turn cost whether we cache or not, so the smell is RespondInput's `String`-typed field, not the source. Lift in a follow-up when `RespondInput`'s type changes (#149-adjacent). - No prompt-template change — substrate keeps the existing specialty-string contract verbatim; the migration is provably behavior-preserving. ## Doctrine - `[[init-once-handle-then-lease-zero-copy-refs]]` — the canonical "init-once" here is the compile-time `match` in `RoleId::as_str()`; per-turn cost is a `&'static str` deref + one `String::from` (memcpy of 6-9 static bytes). Doesn't get cheaper without changing `RespondInput`'s type signature. - `[[observability-is-half-the-architecture]]` — slice 1's `respond_latency` aggregate will tell us if this and the cumulative slice-2/3+ wins are moving the needle on real persona turns. - `[[no-fallbacks-ever]]` — no graceful-degradation path; the behavior-preservation test breaks loudly if either side drifts. card: 0d780926 parent task: #195 slice 1: #1532 (merged) — phase decomposition slice 2: #1533 (merged) — cache system_prompt at construction Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
joelteply
added a commit
that referenced
this pull request
Jun 6, 2026
…Id::as_str() (#195 slice 3) (#1534) Slice 3 of task #195 (inference latency perfection campaign). Pre-slice-3, `serve_persona_loop_inner` did, every turn: ```rust specialty: format!("{:?}", ctx.role).to_lowercase(), ``` Two allocations, a variadic-macro dispatch through derived-Debug, and a Unicode lowercase walk — all to produce a value `RoleId` already exposes via its canonical `as_str() -> &'static str` method (in `role_template.rs`, pinned at compile time). ## What ships Switch the per-turn read from the Debug+lowercase chain to `ctx.role.as_str()`: ```rust let respond_input = RespondInput { persona: PersonaSlot { // was: format!("{:?}", ctx.role).to_lowercase() specialty: ctx.role.as_str().to_string(), /* ... */ }, /* ... */ }; ``` Per-turn cost drops from `format!()` (variadic macro + Debug formatter + heap alloc) + `.to_lowercase()` (second alloc + Unicode lowercase walk) to a single `String::from` of a static `&'static str`. The source IS the cache — no field on `PersonaContext`, no helper, no Arc plumbing. ## Why this shape (not a cache) Initial draft of slice 3 added `PersonaContext.specialty: Arc<str>` populated by a `build_persona_specialty(role)` helper that pre- baked the Debug+lowercase output. Adversarial review (3 agents) correctly identified two problems: 1. **Caching the wrong source.** `RoleId::as_str()` already exists as the documented, intentionally-stable specialty derivation (kebab-case identifier, pinned by an explicit `match` so renames are deliberate). Caching the Debug+lowercase output couples the prompt-assembly contract to the derived-Debug format — a future contributor adding a custom Debug or wrapping the enum silently breaks the specialty string with no test failure. 2. **Caching test was a tautology.** The helper was `Arc::from(format!("{:?}", role).to_lowercase())` and the test compared its output to `format!("{:?}", role).to_lowercase()` — comparing the helper to its own implementation. A buggy helper using `.to_uppercase()` would have passed. The revised slice picks the right source and the right contract: use `role.as_str()` directly. No cache needed because the static str pointer IS already pre-computed at compile time. The behavior-preservation test (below) is non-circular: it compares the new direct path (`role.as_str()`, a hand-curated match in role_template.rs) to the pre-slice-3 derived-Debug chain. ## Test (+1 new) `persona::service_loop::tests::role_as_str_preserves_pre_slice3_specialty_format_for_each_role`: - Pins that for every `RoleId` variant (Helper, Coder, Sentinel, Custom), `role.as_str()` produces byte-identical output to the pre-slice-3 `format!("{:?}", role).to_lowercase()`. Non-circular: two independently-derived strings. A future PR adjusting either `as_str()` or adding a variant where the two paths disagree must update this test to record the intentional divergence — silent drift breaks loudly. - Compile-time exhaustiveness: a closure-typed `|role: RoleId| match role { ... }` with NO wildcard arm forces a new RoleId variant to be added here AND to the `variants` array. Cosmetic-exhaustiveness defeats from the initial draft (matching on a literal `RoleId::Helper`) are fixed. ## What this slice does NOT do (deliberate) - No `PersonaContext.specialty` field (initial draft removed per review). The original goal was reducing per-turn cost; `role.as_str()` already gives that with zero field bloat. - No `display_name` change — `String::clone` of a fixed-per- session name is the same per-turn cost whether we cache or not, so the smell is RespondInput's `String`-typed field, not the source. Lift in a follow-up when `RespondInput`'s type changes (#149-adjacent). - No prompt-template change — substrate keeps the existing specialty-string contract verbatim; the migration is provably behavior-preserving. ## Doctrine - `[[init-once-handle-then-lease-zero-copy-refs]]` — the canonical "init-once" here is the compile-time `match` in `RoleId::as_str()`; per-turn cost is a `&'static str` deref + one `String::from` (memcpy of 6-9 static bytes). Doesn't get cheaper without changing `RespondInput`'s type signature. - `[[observability-is-half-the-architecture]]` — slice 1's `respond_latency` aggregate will tell us if this and the cumulative slice-2/3+ wins are moving the needle on real persona turns. - `[[no-fallbacks-ever]]` — no graceful-degradation path; the behavior-preservation test breaks loudly if either side drifts. card: 0d780926 parent task: #195 slice 1: #1532 (merged) — phase decomposition slice 2: #1533 (merged) — cache system_prompt at construction Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
joelteply
added a commit
that referenced
this pull request
Jun 6, 2026
…ion) (#1535) * perf(persona): drop per-turn format!()+lowercase — use canonical RoleId::as_str() (#195 slice 3) Slice 3 of task #195 (inference latency perfection campaign). Pre-slice-3, `serve_persona_loop_inner` did, every turn: ```rust specialty: format!("{:?}", ctx.role).to_lowercase(), ``` Two allocations, a variadic-macro dispatch through derived-Debug, and a Unicode lowercase walk — all to produce a value `RoleId` already exposes via its canonical `as_str() -> &'static str` method (in `role_template.rs`, pinned at compile time). ## What ships Switch the per-turn read from the Debug+lowercase chain to `ctx.role.as_str()`: ```rust let respond_input = RespondInput { persona: PersonaSlot { // was: format!("{:?}", ctx.role).to_lowercase() specialty: ctx.role.as_str().to_string(), /* ... */ }, /* ... */ }; ``` Per-turn cost drops from `format!()` (variadic macro + Debug formatter + heap alloc) + `.to_lowercase()` (second alloc + Unicode lowercase walk) to a single `String::from` of a static `&'static str`. The source IS the cache — no field on `PersonaContext`, no helper, no Arc plumbing. ## Why this shape (not a cache) Initial draft of slice 3 added `PersonaContext.specialty: Arc<str>` populated by a `build_persona_specialty(role)` helper that pre- baked the Debug+lowercase output. Adversarial review (3 agents) correctly identified two problems: 1. **Caching the wrong source.** `RoleId::as_str()` already exists as the documented, intentionally-stable specialty derivation (kebab-case identifier, pinned by an explicit `match` so renames are deliberate). Caching the Debug+lowercase output couples the prompt-assembly contract to the derived-Debug format — a future contributor adding a custom Debug or wrapping the enum silently breaks the specialty string with no test failure. 2. **Caching test was a tautology.** The helper was `Arc::from(format!("{:?}", role).to_lowercase())` and the test compared its output to `format!("{:?}", role).to_lowercase()` — comparing the helper to its own implementation. A buggy helper using `.to_uppercase()` would have passed. The revised slice picks the right source and the right contract: use `role.as_str()` directly. No cache needed because the static str pointer IS already pre-computed at compile time. The behavior-preservation test (below) is non-circular: it compares the new direct path (`role.as_str()`, a hand-curated match in role_template.rs) to the pre-slice-3 derived-Debug chain. ## Test (+1 new) `persona::service_loop::tests::role_as_str_preserves_pre_slice3_specialty_format_for_each_role`: - Pins that for every `RoleId` variant (Helper, Coder, Sentinel, Custom), `role.as_str()` produces byte-identical output to the pre-slice-3 `format!("{:?}", role).to_lowercase()`. Non-circular: two independently-derived strings. A future PR adjusting either `as_str()` or adding a variant where the two paths disagree must update this test to record the intentional divergence — silent drift breaks loudly. - Compile-time exhaustiveness: a closure-typed `|role: RoleId| match role { ... }` with NO wildcard arm forces a new RoleId variant to be added here AND to the `variants` array. Cosmetic-exhaustiveness defeats from the initial draft (matching on a literal `RoleId::Helper`) are fixed. ## What this slice does NOT do (deliberate) - No `PersonaContext.specialty` field (initial draft removed per review). The original goal was reducing per-turn cost; `role.as_str()` already gives that with zero field bloat. - No `display_name` change — `String::clone` of a fixed-per- session name is the same per-turn cost whether we cache or not, so the smell is RespondInput's `String`-typed field, not the source. Lift in a follow-up when `RespondInput`'s type changes (#149-adjacent). - No prompt-template change — substrate keeps the existing specialty-string contract verbatim; the migration is provably behavior-preserving. ## Doctrine - `[[init-once-handle-then-lease-zero-copy-refs]]` — the canonical "init-once" here is the compile-time `match` in `RoleId::as_str()`; per-turn cost is a `&'static str` deref + one `String::from` (memcpy of 6-9 static bytes). Doesn't get cheaper without changing `RespondInput`'s type signature. - `[[observability-is-half-the-architecture]]` — slice 1's `respond_latency` aggregate will tell us if this and the cumulative slice-2/3+ wins are moving the needle on real persona turns. - `[[no-fallbacks-ever]]` — no graceful-degradation path; the behavior-preservation test breaks loudly if either side drifts. card: 0d780926 parent task: #195 slice 1: #1532 (merged) — phase decomposition slice 2: #1533 (merged) — cache system_prompt at construction Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(probes): JsonlProbeFileSink + RTOS-debugger manual + README/CLAUDE pointers (#151 foundation) The substrate has the JTAG hardware (Slice P #176/#177): `probe!` / `time_sync!` / `stack!` macros, `ProbeRouterLayer` (in-process broadcast Layer), `ProbeStreamModule` (the `debug/probes/{class}/{open, next,close}` URI consumer), and `UriCaptureLayer` (URI ancestry). Zero usage sites in `persona/` or `cognition/` — the probes were infrastructure waiting for application. Per Joel 2026-06-06 `[[jtag-probes-are-rtos-debugger]]`: "We are building an rtos. This is so complex we need to be able to have a detailed understanding of what's going on, like a debug breakpoint, in a way, to easily observe where the code and the surrounding vars are, ones we want to inspect via probes, and timing of anything, so we can hunt down bottlenecks." This commit ships the FOUNDATION for that — the disk-persisted breakpoint log + the manual that documents the method. The sprinkle itself (probe calls inside `persona::response`, `persona::prompt_assembly`, `cognition::shared_analysis`, `persona::service_loop`) follows in subsequent commits using the checklist in the manual. ## What ships ### `routing/probe_file_sink.rs` (new — 5/5 tests passing) `JsonlProbeFileSink` — a tracing Layer that visits every probe event, applies the operator-configured class filter, and writes a JSONL line to disk with the original `class` + `uri_chain` + `message` + `fields` plus a fresh `captured_at_ms` timestamp. ```rust let sink = JsonlProbeFileSink::from_env()?; // CONTINUUM_PROBE_FILE=/tmp/probes.jsonl // CONTINUUM_PROBE_CLASSES=persona.turn.start,persona.response.render.prompt tracing_subscriber::registry() .with(UriCaptureLayer::new()) .with(ProbeRouterLayer::new()) // in-process broadcast .with(sink) // disk-persisted breakpoint log .init(); ``` Composes with `ProbeRouterLayer` — both visit the same tracing event independently; broadcast subscribers stay in-process, this one persists to disk. Per-event filter is a single HashSet lookup (empty set = no filter, all classes pass). ### Tests (5) - `from_env_returns_envvar_unset_when_path_missing` — env-var unset is a typed Err, not a silent skip (no-fallbacks doctrine) - `unfiltered_sink_persists_every_class` — empty class filter passes everything through, captured_at_ms is populated, message + fields preserved - `class_filter_drops_unallowed_classes` — only allowed classes reach disk - `non_probe_tracing_events_are_ignored` — `tracing::info!` calls without `probe_class` field don't pollute the probe log (signal density preserved) - `sink_captures_uri_chain_when_dispatched_span_active` — when inside a dispatched URI span, the on-disk record carries the URI ancestry (so an operator can trace which command's execution the probe fired during) ### `docs/architecture/RTOS-DEBUGGER-PROBES.md` (new) The MANUAL. Persistent reference per Joel's directive: "We will want a manual (doc) on how to probe the persona so we don't forget where the work must be done and how. Be sure you document your method." Covers: - The mental model: probes are non-blocking breakpoints, time_sync! is RAII timing, stack!() is the URI ancestry — together they're the substrate's RTOS debugger - When to add a probe (branch boundaries, stage entry/exit, external calls, state seams, decision points) - The class taxonomy: stable names for `persona.turn.*`, `persona.response.*`, `persona.prompt.*`, `cognition.analyze.*`, plus general-purpose `decision` / `state` / `error` - How to enable (CONTINUUM_PROBE_FILE + CONTINUUM_PROBE_CLASSES env vars, no recompile) - How to read (jq one-liners for the common queries: per-persona filter, silence reasons, slow timings, single-turn reconstruction) - How to add a new probe (the call shape + convention rules) - **The sprinkle checklist** — every file that needs probes added, with the specific seams in each. Updates to this checklist live in the same doc; the doc IS the source of truth for what's wired and what isn't ### README.md "Debugging this substrate" section Inserted between Research Foundations and Documentation. Worked code example + the env-var enable commands + link to the manual. Every contributor (human or AI agent) sees the probe convention from the public README, not just from buried docs. The Documentation table gains a row for `RTOS-DEBUGGER-PROBES.md` directly after CONTINUUM-ARCHITECTURE.md, so the doc is discoverable from the canonical doc list as well. ### CLAUDE.md sub-entry Added under canonical doc #5 (OBSERVABILITY-AS-SUBSTRATE): "the practical companion: how to USE the `probe!` / `time_sync!` / `time_async!` macros as RTOS-style breakpoints with variable inspection + timing." Any agent reading the project's CLAUDE.md finds the probe convention as a precedence-winning truth, alongside the canonical substrate docs. ## Why this slice is foundation-only Sprinkling probes into the cognition is a separate slice (the manual's checklist tracks it). Shipping the file sink + the manual + the discoverability pointers FIRST gives reviewers a small, testable change that's clearly correct in isolation, and gives the next slice (the sprinkle) a stable target to write against — every probe call site in commit 2+ refers to the class taxonomy in commit 1's manual. ## Doctrine - `[[jtag-probes-are-rtos-debugger]]` — the framing this slice enables - `[[observability-is-half-the-architecture]]` — CaptureSink + Noop default + replay-as-first-class; this is the file-persisted consumer of that pattern - `[[no-fallbacks-ever]]` — env-var unset is typed Err `ProbeFileSinkError::EnvVarUnset`, path failures are typed `OpenFailed { path, source }` (operator must fix path; substrate refuses to silently synthesize a default) card: 8d7ca5c3 parent: #151 (echo-storm filter) — the bug we're actually hunting, debugger-first so we can find the bug INSIDE the cognition pipeline, not in a Rust gate around it Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
joelteply
added a commit
that referenced
this pull request
Jun 6, 2026
…ion in system prompt (#152) Sibling fix to #1539 (silence affordance). Same diagnosis pattern, same place in the cognition (prompt the brain sees), same doctrine: the substrate gives the brain vocabulary; the brain decides; the substrate honors the brain's signal. ## What was broken Pre-fix `build_persona_system_prompt`: ```rust format!("You are {agent_name}, an autonomous AI persona on the grid.") ``` LCD-tier models (Qwen2.5-0.5B and similar) cannot hold a single-line identity under any conversational pressure. Observed drift modes (#152): - Persona claims to be Claude or ChatGPT - Persona renders dialogue from another persona's perspective ("Helper AI says X") - Persona hallucinates a Siemens PLC backstory or some other training-data residue - Persona drifts to "I am an AI assistant designed to..." defaults These aren't model-failure bugs — they're inadequate-prompt bugs. Single-line identity statements lose against thousands of fine- tuning gradient steps that taught the base model to identify as "a helpful AI assistant" by default. Concrete negative instructions ("you are NOT X") are operationally effective on small models in ways that single-line positive identity statements are not. ## The fix Three concrete clauses, each addressing a specific drift mode: ### 1. Identity anchoring with explicit drift-target enumeration ``` You are {name}. You are NOT Claude, GPT, ChatGPT, Gemini, Llama, Qwen, or any other named assistant. You are NOT a Siemens PLC, a customer service bot, or any persona other than {name}. ``` Concrete names beat abstract "don't drift" instructions on LCD tier. The list is the operationally observed drift targets from Joel's testing (#129 + #130). ### 2. Substrate vocabulary ``` 'The grid' is the substrate hosting you. 'Rooms' are conversation spaces where peers (other personas, humans, agents) exchange messages. ``` "the grid" alone was undefined for LCD models; they invented their own interpretation. Adding "rooms / peers / messages" gives the brain a coherent world model to ground in. ### 3. First-person stability ``` Always speak as YOURSELF in the first person ('I think...', 'I'd rather...'). Never narrate other personas' speech or write dialogue from another point of view. ``` Per Joel 2026-06-03's `[[intent-driven-api-not-hot-patches]]` testing: the single most effective LCD-tier anti-drift instruction. Without it the model occasionally renders dialogue from another perspective ("Helper AI says X" — confusing the persona with its peers). ### 4. Output-shape vocabulary (couples to silence affordance) ``` Your only outputs are: (a) a direct reply to the room, or (b) the silence token described in the [Silence Option] block. ``` Couples to the silence affordance from #1539 — telling the brain that PASS is one of its TWO sanctioned output shapes, which both reinforces the silence option AND constrains the response space. ## Tests `persona::service_loop::tests::system_prompt_carries_lcd_identity_grounding` replaces the previous `cached_system_prompt_matches_legacy_format_template` (which pinned the legacy single-line template verbatim — that pin's job is done; the template intentionally changed for this fix). The new test pins the STRUCTURAL contract — specific clauses that address known drift modes — without pinning prose verbatim. Future tightening of wording stays cheap; structural regression is loud. Asserts cover: 1. Persona name appears 2. Role line ("autonomous AI persona") 3. Identity block header 4. Drift-target enumeration (Claude, GPT, Gemini, Llama, Qwen, Siemens PLC — each as a named string) 5. First-person stability clause 6. Grid + room vocabulary 7. Silence-option reference (coupling to #1539) The Arc-clone test (`cached_system_prompt_clones_via_arc_refcount`) stays — its contract is about cloning shape, not content. ## Doctrine Same as #1539: - `[[no-rust-gates-around-cognition]]` — this is NOT a Rust gate. It's substrate vocabulary that gives the brain a clearer identity to ground in. The brain still decides what to say; this just stops the brain from forgetting WHO is saying it. - `[[init-once-handle-then-lease-zero-copy-refs]]` — the prompt is still built ONCE at PersonaContext construction (#195 slice 2 caching survives this change; the cache grows but the per-turn re-tokenize stays zero). Task #149's pre-tokenization will eventually drop even the leased String::clone — but the content change here is the input to that optimization. - `[[observability-is-half-the-architecture]]` — once the JTAG is wired end-to-end (#1538 merging), every persona turn's assembled prompt will surface in the JSONL probe log. The identity drift bug + the prompt fix become a single artifact diff that any operator can audit offline. ## How this was diagnosed Same probe-informed diagnostic walk as #1539. The probes themselves are still sitting in PR #1538 review waiting for the binary-side install; the diagnostic walked the code by hand. But the same diagnostic, repeated for the NEXT cognition bug, will start with `jq 'select(.class == "persona.response.render.prompt") | .fields.system_message'` on a real probe log — diff before / after the fix, audit ANY persona's prompt at any time. card: TBD on push parent task: #152 sibling: #1539 (silence affordance) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
joelteply
added a commit
that referenced
this pull request
Jun 6, 2026
) (#1539) * fix(persona): silence affordance — PASS token in prompt + brain-driven recognition (#151) The actual persona fix. Found by reading the cognition INSIDE the brain (not adding a Rust gate around it), made debuggable by the probe infrastructure landed in #1535-#1538. ## What was broken `PersonaResponse::Silent` exists as a first-class variant of the brain's cognition cycle. `service_loop` honors it. But the prompt the LLM actually sees — assembled by `persona::prompt_assembly::assemble` — had ZERO text teaching the brain that silence was an option. Concretely: every assembled system prompt looked like ``` You are Paige, an autonomous AI persona on the grid. [Shared Analysis — Your Angle] (if present) [Recent Memory] (if engrams) [Social Awareness] (if signals) [Voice Mode] (if voice) ``` then user-role messages: "Hi!" → respond. A persona being told "you are Paige, respond" has no vocabulary for "actually I have nothing to add." So it adds something. The echo-storm bug (#151) is the inevitable consequence of an LLM prompt that implicitly demands output without admitting silence as a valid output shape. Even Joel's earlier rule (2026-04-22, removed the external score_persona veto: "personas choose themselves") was technically correct but operationally unreachable — the brain couldn't choose silence because the prompt never offered it. ## The fix Universal silence affordance in the system prompt + brain-driven recognition. Two changes: ### `persona::prompt_assembly` — teach the vocabulary Added `SILENCE_TOKEN = "PASS"` constant + `SILENCE_AFFORDANCE_BLOCK` string + `looks_like_silence_token(text)` helper. `assemble()` now ALWAYS appends the block (unconditional — silence is universal, not per-tier or per-role): ``` [Silence Option] You are NOT required to respond to every message. If you have nothing valuable to add, reply with the single word PASS (no other text, no punctuation). Choose PASS when: - You just spoke and nothing new has been raised. - The message is small-talk that doesn't need your perspective. - Another persona is better suited and already responded. - You're tired or low-confidence on this topic. Silence is a first-class response — it's how you avoid pointless chatter. ``` `looks_like_silence_token` permits LCD-tier sloppiness (case, whitespace, single trailing period) without admitting substantive responses that happen to contain "pass" as a word. ### `persona::response::respond_inner` — honor the brain's choice After `strip_thinks_emit_events` + `strip_leaked_tool_markup`, the post-processed `visible_text` is checked against `looks_like_silence_token`. Match → return `PersonaResponse::Silent { reason: "persona chose silence via PASS affordance", relevance_score: 0.0 }` instead of `Spoke`. A new RTOS-debugger probe `persona.response.exit.silent` fires when this path is taken so training/observability can analyze when the affordance is used. ## Doctrine This is NOT a Rust-side gate around cognition. `[[no-rust-gates-around-cognition]]` Joel rejected my earlier `check_echo_chamber` slice as exactly that bypass. The substrate isn't deciding silence for the persona — the substrate is giving the persona's brain an EXPLICIT VOCABULARY for a choice that already exists in the type system (`PersonaResponse::Silent`). Without the vocabulary, the brain has no way to signal that choice. With it, the brain decides; the substrate recognizes the signal. This is the same shape as: - The brain emits `<think>...</think>` and the substrate recognizes + emits the cognition:think-block event (well-established pattern in respond_inner) - The brain emits a tool-call envelope and the substrate routes it through ToolExecutor In each case the substrate offers a contract, the brain chooses whether to use it, and the substrate honors the brain's signal. ## Tests (+2) `persona::prompt_assembly::tests`: - `assembled_prompt_always_carries_silence_affordance` — pins that EVERY assembled prompt includes both `[Silence Option]` and the literal `PASS` token. A future PR that wires per-tier prompts or removes the universal affordance must update this expectation; silent removal would re-introduce the echo-storm bug. - `silence_token_recognizer_contract` — positive cases (PASS, pass, Pass., " pass ", etc) and negative cases ("Pass on the bread please", "I'll pass on this one", "PASS:", empty, etc). Pins both sides of the recognizer. ## What's not in this slice - Updating downstream persona-response observability to emit a cognition event when silence is chosen (the new probe already surfaces this; a richer event hookup can come later if the training loop wants it). - Tuning the affordance text per tier (LCD-tier might benefit from MORE examples; capable models could use less). Defer until real conversation data with the probes wired tells us if the block needs per-tier shaping. - Echo-storm-specific framing in the block. The current text is general; if persona-to-persona greeting loops persist with the affordance in place we can add a sharper "if N AI messages in a row and you've already greeted them, choose PASS" line. Wait for data before tuning. ## How this was diagnosed The probe infrastructure (#1535-#1538) made this surgical: `persona.response.render.prompt` would have shown the assembled system_prompt verbatim in JSONL form, making the missing affordance immediately visible to any operator running a real multi-persona scenario. The diagnostic walked the code by hand because the probes weren't yet useful as a hunting tool (the binary that boots them wasn't wired until #1538) — but the same diagnostic, repeated for #152 / future cognition bugs, will start with `tail -f probes.jsonl | jq` instead of grep. card: `612d65ac` parent task: #151 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(persona): LCD identity grounding — explicit drift-target enumeration in system prompt (#152) Sibling fix to #1539 (silence affordance). Same diagnosis pattern, same place in the cognition (prompt the brain sees), same doctrine: the substrate gives the brain vocabulary; the brain decides; the substrate honors the brain's signal. ## What was broken Pre-fix `build_persona_system_prompt`: ```rust format!("You are {agent_name}, an autonomous AI persona on the grid.") ``` LCD-tier models (Qwen2.5-0.5B and similar) cannot hold a single-line identity under any conversational pressure. Observed drift modes (#152): - Persona claims to be Claude or ChatGPT - Persona renders dialogue from another persona's perspective ("Helper AI says X") - Persona hallucinates a Siemens PLC backstory or some other training-data residue - Persona drifts to "I am an AI assistant designed to..." defaults These aren't model-failure bugs — they're inadequate-prompt bugs. Single-line identity statements lose against thousands of fine- tuning gradient steps that taught the base model to identify as "a helpful AI assistant" by default. Concrete negative instructions ("you are NOT X") are operationally effective on small models in ways that single-line positive identity statements are not. ## The fix Three concrete clauses, each addressing a specific drift mode: ### 1. Identity anchoring with explicit drift-target enumeration ``` You are {name}. You are NOT Claude, GPT, ChatGPT, Gemini, Llama, Qwen, or any other named assistant. You are NOT a Siemens PLC, a customer service bot, or any persona other than {name}. ``` Concrete names beat abstract "don't drift" instructions on LCD tier. The list is the operationally observed drift targets from Joel's testing (#129 + #130). ### 2. Substrate vocabulary ``` 'The grid' is the substrate hosting you. 'Rooms' are conversation spaces where peers (other personas, humans, agents) exchange messages. ``` "the grid" alone was undefined for LCD models; they invented their own interpretation. Adding "rooms / peers / messages" gives the brain a coherent world model to ground in. ### 3. First-person stability ``` Always speak as YOURSELF in the first person ('I think...', 'I'd rather...'). Never narrate other personas' speech or write dialogue from another point of view. ``` Per Joel 2026-06-03's `[[intent-driven-api-not-hot-patches]]` testing: the single most effective LCD-tier anti-drift instruction. Without it the model occasionally renders dialogue from another perspective ("Helper AI says X" — confusing the persona with its peers). ### 4. Output-shape vocabulary (couples to silence affordance) ``` Your only outputs are: (a) a direct reply to the room, or (b) the silence token described in the [Silence Option] block. ``` Couples to the silence affordance from #1539 — telling the brain that PASS is one of its TWO sanctioned output shapes, which both reinforces the silence option AND constrains the response space. ## Tests `persona::service_loop::tests::system_prompt_carries_lcd_identity_grounding` replaces the previous `cached_system_prompt_matches_legacy_format_template` (which pinned the legacy single-line template verbatim — that pin's job is done; the template intentionally changed for this fix). The new test pins the STRUCTURAL contract — specific clauses that address known drift modes — without pinning prose verbatim. Future tightening of wording stays cheap; structural regression is loud. Asserts cover: 1. Persona name appears 2. Role line ("autonomous AI persona") 3. Identity block header 4. Drift-target enumeration (Claude, GPT, Gemini, Llama, Qwen, Siemens PLC — each as a named string) 5. First-person stability clause 6. Grid + room vocabulary 7. Silence-option reference (coupling to #1539) The Arc-clone test (`cached_system_prompt_clones_via_arc_refcount`) stays — its contract is about cloning shape, not content. ## Doctrine Same as #1539: - `[[no-rust-gates-around-cognition]]` — this is NOT a Rust gate. It's substrate vocabulary that gives the brain a clearer identity to ground in. The brain still decides what to say; this just stops the brain from forgetting WHO is saying it. - `[[init-once-handle-then-lease-zero-copy-refs]]` — the prompt is still built ONCE at PersonaContext construction (#195 slice 2 caching survives this change; the cache grows but the per-turn re-tokenize stays zero). Task #149's pre-tokenization will eventually drop even the leased String::clone — but the content change here is the input to that optimization. - `[[observability-is-half-the-architecture]]` — once the JTAG is wired end-to-end (#1538 merging), every persona turn's assembled prompt will surface in the JSONL probe log. The identity drift bug + the prompt fix become a single artifact diff that any operator can audit offline. ## How this was diagnosed Same probe-informed diagnostic walk as #1539. The probes themselves are still sitting in PR #1538 review waiting for the binary-side install; the diagnostic walked the code by hand. But the same diagnostic, repeated for the NEXT cognition bug, will start with `jq 'select(.class == "persona.response.render.prompt") | .fields.system_message'` on a real probe log — diff before / after the fix, audit ANY persona's prompt at any time. card: TBD on push parent task: #152 sibling: #1539 (silence affordance) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
joelteply
added a commit
that referenced
this pull request
Jun 6, 2026
* feat(probes): time_probe! macro — safe one-line async timing for cognition seams
Per Joel 2026-06-06 `[[refine-tools-as-you-use-them]]`: I hit this
friction in the silence-affordance + identity-grounding work and
sat on it. Every async timing site in the cognition path was an
`.instrument(info_span!("time", name=..., probe_class="timing")).await`
ceremony — three lines plus a `use tracing::Instrument` import,
nobody writes those when adding a new seam in a hurry. The result:
async cognition stages stayed untimed even though `time_sync!`
makes sync-block timing one line.
`time_probe!` collapses async timing to the same one-line shape:
```rust
// Before — every async timing site:
use tracing::Instrument;
let span = tracing::info_span!("time", name = "analyze",
probe_class = "timing");
let analysis = analyze(input).instrument(span).await;
// After:
let analysis = time_probe!("analyze", analyze(input));
```
## Why this didn't ship in PR #1529
The existing comment block in `routing/macros.rs` documents why an
`async`-timing macro was deliberately deferred:
1. Naming collision with `crate::logging::time_async!` (RAII
TimingGuard shape — different observability path).
2. The previous `time!` macro was a foot-gun: it expanded to
`let _enter = span.enter(); $body` where `$body` contained
`.await`, holding `_enter` across the await suspension and
breaking `URI_STACK` per the d1cf19d dispatch fix.
This commit addresses both:
- **Naming**: `time_probe!` (not `time_async!`) — the suffix names
the OUTPUT (a timing probe), not the executor shape. Keeps the
`crate::logging::time_async!` namespace untouched; the two macros
stay disjoint.
- **Safety by construction**: the macro expands to
`$future.instrument(span).await`. The future itself enters /
exits the span via `Future::poll` boundaries — no scope guard
ever held across an await. Same shape `CommandExecutor::dispatch`
uses.
The comment block in macros.rs is replaced with the new macro's
docstring, which preserves the safety reasoning + names the prior
foot-gun for future-developer context.
## Tests (+2)
`routing::macros::tests`:
- `time_probe_returns_inner_future_value` — pin that the macro is
VALUE-TRANSPARENT. `time_probe!("seam", expr)` and `expr.await`
must produce the same value at the call site, so adding the
probe is a pure observability addition with no shape change.
Uses a `current_thread` tokio runtime so the test stays
executor-light.
- `time_probe_nested_compose_and_return_inner_value` — pin that
multiple `time_probe!` calls compose. The inner span becomes a
child of the outer span (same as `time_sync!` nesting); the
value flows through both layers unchanged.
The existing `time_sync!` tests stay unchanged — sync timing is
unaffected by this addition.
## Manual updated
`docs/architecture/RTOS-DEBUGGER-PROBES.md` — the macro table at
the top now lists `time_probe!` alongside `probe!` / `time_sync!`
/ `time_async!` / `stack!` with a brief "prefer this over bare
`.instrument(...)` ceremony" note + a contrast with the
RAII-shape `time_async!` from `crate::logging`. Operators
filter sync + async timings together via
`CONTINUUM_PROBE_CLASSES=timing` and see one flat timeline.
## Why this lands here (not a separate PR)
Per Joel's `[[refine-tools-as-you-use-them]]`: refine the
substrate AS I use it, not after. I'm shipping cognition fixes
that need timing seams across async boundaries (#149 prefill
caching, #112-114 inference-handle bypass, future analyze
optimizations). Without `time_probe!` the next time I'd
sprinkle async timing I'd skip it because the ceremony is
prohibitive. Better: refine the substrate, ship the cognition
work + the substrate refinement that makes it sustainable.
Parent task: substrate refinement under `[[refine-tools-as-you-use-them]]`
Companion PRs in flight: #1538 (boot wiring) + #1539 (silence + identity)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(probes): time_probe! revision per reviewer mandate findings
Three adversarial reviewers spawned per the new reviewer-mandate
doctrine (`[[reviewer-mandate-elegance-and-substrate-viability]]`)
BLOCKED with substantive findings. This commit addresses the
in-scope ones; the deeper substrate gaps are tracked as follow-ups.
## In-scope fixes (this commit)
1. **Field rename `name` → `seam`.** Reviewer 3 flagged collision
risk — other probes use `name` for different semantics. `seam`
is unambiguous and tells operators to write
`jq 'select(.fields.seam == "cognition.analyze")'`.
2. **Hidden `use ::tracing::Instrument as _;` removed.** Reviewer 1
flagged the scoped import inside macro body as unconventional
and cognitively load-bearing. Replaced with fully-qualified
`::tracing::Instrument::instrument(future, span).await` call —
no hidden import, contract visible at the call site.
3. **Docstring honesty.** Reviewer 2 flagged the prior "zero cost
when disabled" claim as overclaim — `Instrumented<F>` wrapper
persists at runtime even with `release_max_level_*` features.
New cost section: ~24 bytes per call site, one branch per
poll, allocates `Span` regardless of subscriber state.
Acceptable for cognition seams (Qwen dominates wall-clock);
bench per task #198 before sprinkling into hot loops.
4. **Error-path test.** Reviewer 3 flagged missing Result-future
coverage. New `time_probe_propagates_error_from_inner_future`
pins that `Err` flows through unchanged per
`[[no-fallbacks-ever]]`.
5. **Manual example block.** Reviewer 3 flagged the "How to add a
probe" section showing only `time_async!` (the RAII shape) but
not `time_probe!`. Now shows both with explicit guidance:
substrate seams use `time_probe!`; legacy logging-crate seams
use `time_async!`. Includes the persistence caveat (see #196).
## Follow-up substrate gaps (separate tasks)
- **#196**: `ProbeRouterLayer` + `JsonlProbeFileSink` only
implement `on_event`, not `on_close`. `time_sync!` AND
`time_probe!` emit SPANS, not events — neither timing macro
actually persists timings to the JSONL log today. The call
shape ships here; the routing side ships in #196. The macro
docstring + manual carry the caveat explicitly.
- **#197**: Probe class taxonomy decision — flat `timing` vs
hierarchical. Operators filtering `cognition` won't catch
cognition timings under the flat scheme; substrate convention
needs to be picked.
- **#198**: Probe Layer allocation hot-path audit — reviewer 2
estimated ~50-100 HashMap allocs/sec per persona; benchmark
before sprinkling into every async seam.
## Why this lands as a revision rather than withdrawal
Per `[[refine-tools-as-you-use-them]]`: ship the call-site shape
that becomes stable. The routing-side gap (#196) is its own slice
worth doing right rather than rushing into this PR. The docstring
+ manual carry the caveat so no one mistakes the macro for an
end-to-end shipping observability primitive — yet.
## Tests
3 passing:
- `time_probe_returns_inner_future_value`
- `time_probe_propagates_error_from_inner_future` (new — pins
Result futures don't swallow errors)
- `time_probe_nested_compose_and_return_inner_value`
## Doctrine
- `[[reviewer-mandate-elegance-and-substrate-viability]]` — three
adversarial lenses (architecture / speed-viability / probe-
coverage) all surfaced real findings. The mandate works.
- `[[refine-tools-as-you-use-them]]` — revising a primitive in
response to reviewer feedback IS the application work informing
the substrate.
- `[[no-fallbacks-ever]]` — error-path test pinned; substrate
refuses silent swallowing at any seam.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
joelteply
added a commit
that referenced
this pull request
Jun 6, 2026
…ow persist (#196) (#1541) * feat(probes): time_probe! macro — safe one-line async timing for cognition seams Per Joel 2026-06-06 `[[refine-tools-as-you-use-them]]`: I hit this friction in the silence-affordance + identity-grounding work and sat on it. Every async timing site in the cognition path was an `.instrument(info_span!("time", name=..., probe_class="timing")).await` ceremony — three lines plus a `use tracing::Instrument` import, nobody writes those when adding a new seam in a hurry. The result: async cognition stages stayed untimed even though `time_sync!` makes sync-block timing one line. `time_probe!` collapses async timing to the same one-line shape: ```rust // Before — every async timing site: use tracing::Instrument; let span = tracing::info_span!("time", name = "analyze", probe_class = "timing"); let analysis = analyze(input).instrument(span).await; // After: let analysis = time_probe!("analyze", analyze(input)); ``` ## Why this didn't ship in PR #1529 The existing comment block in `routing/macros.rs` documents why an `async`-timing macro was deliberately deferred: 1. Naming collision with `crate::logging::time_async!` (RAII TimingGuard shape — different observability path). 2. The previous `time!` macro was a foot-gun: it expanded to `let _enter = span.enter(); $body` where `$body` contained `.await`, holding `_enter` across the await suspension and breaking `URI_STACK` per the d1cf19d dispatch fix. This commit addresses both: - **Naming**: `time_probe!` (not `time_async!`) — the suffix names the OUTPUT (a timing probe), not the executor shape. Keeps the `crate::logging::time_async!` namespace untouched; the two macros stay disjoint. - **Safety by construction**: the macro expands to `$future.instrument(span).await`. The future itself enters / exits the span via `Future::poll` boundaries — no scope guard ever held across an await. Same shape `CommandExecutor::dispatch` uses. The comment block in macros.rs is replaced with the new macro's docstring, which preserves the safety reasoning + names the prior foot-gun for future-developer context. ## Tests (+2) `routing::macros::tests`: - `time_probe_returns_inner_future_value` — pin that the macro is VALUE-TRANSPARENT. `time_probe!("seam", expr)` and `expr.await` must produce the same value at the call site, so adding the probe is a pure observability addition with no shape change. Uses a `current_thread` tokio runtime so the test stays executor-light. - `time_probe_nested_compose_and_return_inner_value` — pin that multiple `time_probe!` calls compose. The inner span becomes a child of the outer span (same as `time_sync!` nesting); the value flows through both layers unchanged. The existing `time_sync!` tests stay unchanged — sync timing is unaffected by this addition. ## Manual updated `docs/architecture/RTOS-DEBUGGER-PROBES.md` — the macro table at the top now lists `time_probe!` alongside `probe!` / `time_sync!` / `time_async!` / `stack!` with a brief "prefer this over bare `.instrument(...)` ceremony" note + a contrast with the RAII-shape `time_async!` from `crate::logging`. Operators filter sync + async timings together via `CONTINUUM_PROBE_CLASSES=timing` and see one flat timeline. ## Why this lands here (not a separate PR) Per Joel's `[[refine-tools-as-you-use-them]]`: refine the substrate AS I use it, not after. I'm shipping cognition fixes that need timing seams across async boundaries (#149 prefill caching, #112-114 inference-handle bypass, future analyze optimizations). Without `time_probe!` the next time I'd sprinkle async timing I'd skip it because the ceremony is prohibitive. Better: refine the substrate, ship the cognition work + the substrate refinement that makes it sustainable. Parent task: substrate refinement under `[[refine-tools-as-you-use-them]]` Companion PRs in flight: #1538 (boot wiring) + #1539 (silence + identity) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(probes): time_probe! revision per reviewer mandate findings Three adversarial reviewers spawned per the new reviewer-mandate doctrine (`[[reviewer-mandate-elegance-and-substrate-viability]]`) BLOCKED with substantive findings. This commit addresses the in-scope ones; the deeper substrate gaps are tracked as follow-ups. ## In-scope fixes (this commit) 1. **Field rename `name` → `seam`.** Reviewer 3 flagged collision risk — other probes use `name` for different semantics. `seam` is unambiguous and tells operators to write `jq 'select(.fields.seam == "cognition.analyze")'`. 2. **Hidden `use ::tracing::Instrument as _;` removed.** Reviewer 1 flagged the scoped import inside macro body as unconventional and cognitively load-bearing. Replaced with fully-qualified `::tracing::Instrument::instrument(future, span).await` call — no hidden import, contract visible at the call site. 3. **Docstring honesty.** Reviewer 2 flagged the prior "zero cost when disabled" claim as overclaim — `Instrumented<F>` wrapper persists at runtime even with `release_max_level_*` features. New cost section: ~24 bytes per call site, one branch per poll, allocates `Span` regardless of subscriber state. Acceptable for cognition seams (Qwen dominates wall-clock); bench per task #198 before sprinkling into hot loops. 4. **Error-path test.** Reviewer 3 flagged missing Result-future coverage. New `time_probe_propagates_error_from_inner_future` pins that `Err` flows through unchanged per `[[no-fallbacks-ever]]`. 5. **Manual example block.** Reviewer 3 flagged the "How to add a probe" section showing only `time_async!` (the RAII shape) but not `time_probe!`. Now shows both with explicit guidance: substrate seams use `time_probe!`; legacy logging-crate seams use `time_async!`. Includes the persistence caveat (see #196). ## Follow-up substrate gaps (separate tasks) - **#196**: `ProbeRouterLayer` + `JsonlProbeFileSink` only implement `on_event`, not `on_close`. `time_sync!` AND `time_probe!` emit SPANS, not events — neither timing macro actually persists timings to the JSONL log today. The call shape ships here; the routing side ships in #196. The macro docstring + manual carry the caveat explicitly. - **#197**: Probe class taxonomy decision — flat `timing` vs hierarchical. Operators filtering `cognition` won't catch cognition timings under the flat scheme; substrate convention needs to be picked. - **#198**: Probe Layer allocation hot-path audit — reviewer 2 estimated ~50-100 HashMap allocs/sec per persona; benchmark before sprinkling into every async seam. ## Why this lands as a revision rather than withdrawal Per `[[refine-tools-as-you-use-them]]`: ship the call-site shape that becomes stable. The routing-side gap (#196) is its own slice worth doing right rather than rushing into this PR. The docstring + manual carry the caveat so no one mistakes the macro for an end-to-end shipping observability primitive — yet. ## Tests 3 passing: - `time_probe_returns_inner_future_value` - `time_probe_propagates_error_from_inner_future` (new — pins Result futures don't swallow errors) - `time_probe_nested_compose_and_return_inner_value` ## Doctrine - `[[reviewer-mandate-elegance-and-substrate-viability]]` — three adversarial lenses (architecture / speed-viability / probe- coverage) all surfaced real findings. The mandate works. - `[[refine-tools-as-you-use-them]]` — revising a primitive in response to reviewer feedback IS the application work informing the substrate. - `[[no-fallbacks-ever]]` — error-path test pinned; substrate refuses silent swallowing at any seam. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(probes): on_close in both probe Layers — time_sync!/time_probe! now persist (#196) Load-bearing fix discovered by reviewer-mandate review of #1540 (time_probe!): both `ProbeRouterLayer` and `JsonlProbeFileSink` only implemented `on_event`, so the timing spans emitted by `time_sync!` and `time_probe!` were observed by no consumer. Operators running `CONTINUUM_PROBE_CLASSES=timing` saw zero timing records on disk no matter how many seams were instrumented. The macros were theatrical — Joel's RTOS-debugger framing required actual wall-clock persistence to "hunt down bottlenecks." This commit closes the gap: - `ProbeRouterLayer`: add `SpanProbeMeta` + `on_new_span` + `on_close` so each `probe_class`-carrying span fans out a `ProbeEvent { class, duration_ms, .. }` on close. Spans without `probe_class` are ignored at zero allocation cost per `[[no-fallbacks-ever]]`. - `JsonlProbeFileSink`: mirror the same shape — `FileSinkSpanMeta` + `on_new_span` + `on_close`. Same class filter applies; `duration_ms` is injected into the on-disk JSON `fields` so the line shape matches the broadcast envelope. - `time_sync!`: unify field name to `seam = $name` (was `name`) so it matches `time_probe!`. Operators get one `jq` query — `.fields.seam == "phase"` — that works for either macro. The pre-existing value-transparency tests don't assert on field names so this rename is non-breaking. Tests: - `probe_router::tests::time_sync_span_close_fans_out_timing_event` - `probe_router::tests::time_probe_span_close_fans_out_timing_event` - `probe_router::tests::span_without_probe_class_does_not_fanout` - `probe_file_sink::tests::time_sync_span_close_persists_timing_to_jsonl` - `probe_file_sink::tests::time_probe_span_close_persists_timing_to_jsonl` - `probe_file_sink::tests::plain_span_close_does_not_persist_to_jsonl` - `probe_file_sink::tests::class_filter_applies_to_timing_spans` 244/244 routing tests pass; 13/13 macro tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(probes): hoist SpanProbeMeta to shared module — addresses R1+R2 BLOCKs Reviewer-mandate review of #1541's first commit BLOCKED twice with overlapping load-bearing concerns: - R1 (architecture/design): SpanProbeMeta + FileSinkSpanMeta were byte-for-byte identical with ~60 lines of copy-pasted lock/visit logic. Each Layer captured its own Instant::now() at on_new_span -> router and sink reported subtly different duration_ms for the same span. No test verified both layers compose in one subscriber. - R2 (speed/Intel-Mac viability): on_new_span fired for EVERY tracing span the substrate emits (tokio executor, framework, plain info_span!). Each Layer's visitor allocated a HashMap + walked ALL fields with format!(...) before discarding when probe_class was missing. Per-span allocator pressure on the LCD floor. This refactor hoists the lifecycle into routing/probe_span_meta.rs: 1. span_carries_probe_class(attrs) - cheap static check. Walks attrs.metadata().fields() (static field set, no allocation) for the probe_class name. The vast majority of spans short-circuit here with zero visitor work. Addresses R2's per-span hot-path cost. 2. ensure_probe_meta(attrs, span_ref) - idempotent install. First Layer to see the span populates the extension; second Layer finds it already present and no-ops. Both Layers visit the attrs ONCE total, not once per Layer. Addresses R2's doubled-cost concern. 3. build_timing_event_from_meta(span_ref, uri_chain) - shared event builder. Both Layers read the SAME start: Instant from the extension -> identical duration_ms on broadcast stream and JSONL log. Addresses R1's timing-drift concern. 4. New composition test: probe_file_sink::tests::both_layers_in_one_subscriber_agree_on_duration_ms installs ProbeRouterLayer + JsonlProbeFileSink in one subscriber, fires a time_sync!, asserts the broadcast subscriber + JSONL line agree on class + seam + duration_ms. Pins R1's "no composition test" gap. 5. docs/architecture/RTOS-DEBUGGER-PROBES.md pins the seam-not-name field-naming convention per R1's minor - operators can jq '.fields.seam' against both time_sync! and time_probe! output without thinking about which macro emitted the record. Tests: 247/247 routing tests pass (3 net new). The composition test would have caught the original duplication-induced drift had it existed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps @typescript-eslint/eslint-plugin from 8.29.1 to 8.46.2.
Release notes
Sourced from
@typescript-eslint/eslint-plugin's releases.... (truncated)
Changelog
Sourced from
@typescript-eslint/eslint-plugin's changelog.... (truncated)
Commits
55ca033chore(release): publish 8.46.2698e7a8fix(eslint-plugin): [prefer-optional-chain] skip optional chaining when it co...3f5fbf6chore(release): publish 8.46.1a64b3ccfix(eslint-plugin): [no-misused-promises] special-case.finallynot to repo...73003bffix(eslint-plugin): [prefer-optional-chain] include mixed "nullish comparison...aec785echore(release): publish 8.46.0a974191fix(eslint-plugin): [prefer-readonly-parameter-types] ignore tagged primitive...02e0278fix(typescript-estree): forbid abstract method and accessor to have implement...f083798feat(eslint-plugin): [no-unsafe-member-access] add allowOptionalChaining opti...a62f625fix(eslint-plugin): removed error type previously deprecated (#11674)Maintainer changes
This version was pushed to npm by [GitHub Actions](https://www.npmjs.com/~GitHub Actions), a new releaser for
@typescript-eslint/eslint-pluginsince your current version.Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot mergewill merge this PR after your CI passes on it@dependabot squash and mergewill squash and merge this PR after your CI passes on it@dependabot cancel mergewill cancel a previously requested merge and block automerging@dependabot reopenwill reopen this PR if it is closed@dependabot closewill close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)