Skip to content

docs(architecture): codify recipe activity graph semantics - #1268

Merged
joelteply merged 1 commit into
canaryfrom
docs/recipe-room-activity-graph-1266
May 15, 2026
Merged

docs(architecture): codify recipe activity graph semantics#1268
joelteply merged 1 commit into
canaryfrom
docs/recipe-room-activity-graph-1266

Conversation

@joelteply

Copy link
Copy Markdown
Contributor

Summary

  • document that recipes are reusable templates, not live room/activity state
  • define room/activity as the same instantiated graph node from social vs workflow angles
  • require parent/child activities to use references/edges rather than copied nested state
  • cross-link the invariant from ForgeRecipe, room/activity, and recipe architecture docs

Validation

  • precommit: TypeScript build + browser ping
  • pre-push: TypeScript + ESLint ratchet

Closes #1266

@joelteply
joelteply merged commit 006f6c8 into canary May 15, 2026
2 checks passed
@joelteply
joelteply deleted the docs/recipe-room-activity-graph-1266 branch May 15, 2026 16:09
joelteply added a commit that referenced this pull request Jun 9, 2026
…task #143 slice 2) (#1561)

* perf(concurrency,#1235): refcount-per-key cleanup so analyzer cancellation can't drop entry mid-flight (#1244)

* perf(concurrency,#1235): refcount-per-key cleanup so analyzer cancellation can't drop entry mid-flight

Pre-#1235 only the analyzer (first caller for a key) held the Drop
guard for the in_flight entry. That correctly fixed the panic-cleanup
case (#1232) but left a window during analyzer cancellation:

  T0: analyzer.single_flight("k") creates entry, holds guard
  T1: awaiter1.single_flight("k") clones the Shared, no guard
  T2: analyzer task is cancelled
  T3: analyzer's guard.drop fires, removes entry from in_flight
  T4: NEW caller.single_flight("k") finds no entry, starts FRESH
      work — duplicate inference for the same key, contract violated.
      awaiter1 still completes the original Shared.

Codex flagged this on #1233.

This change makes EVERY caller (analyzer + awaiters) hold a
RefCountGuard. The HashMap value becomes KeyEntry { shared, refcount:
Arc<AtomicUsize> }. Each caller bumps the refcount under the in_flight
lock when constructing its guard; each guard drops decrement it. The
entry is removed only when refcount hits zero — and only after a
double-check under the lock to handle the race where a brand-new caller
bumps the refcount between fetch_sub and lock acquisition.

Behavior preserved:
- Single producer for many waiters: same as before.
- Panic cleanup (#1232): work-future panic re-raises through every
  Shared clone; all guards drop during unwind, refcount → 0, entry
  removed.

Compiles clean. Tests follow in the next commit.

* test(concurrency,#1235): two cancellation-race tests for refcount cleanup

Two new tests proving the #1235 fix:

1. analyzer_cancellation_does_not_evict_entry_while_awaiters_hold_it
   - Analyzer + awaiter both register for the same key.
   - Analyzer task is cancelled (abort).
   - Awaiter is still holding the Shared.
   - A NEW caller arrives for the same key.
   - Asserts: in_flight_count stays 1 across the analyzer drop;
     work-future producer body runs EXACTLY ONCE across all three
     callers; new caller's result equals the analyzer's original
     result (joined the same Shared, didn't start fresh).

   Pre-#1235 this would have failed: analyzer's guard drop would have
   removed the in_flight entry, and the new caller would have started
   duplicate work (producers count == 2, not 1).

2. all_callers_cancelled_evicts_entry_for_fresh_start
   - Two callers register, both cancelled before completion.
   - Asserts: refcount → 0, entry evicted.
   - Fresh caller for the same key starts a fresh work future
     (the prior abandoned work is gone).

Both tests run on tokio multi-threaded runtime (default) so abort
+ Shared interactions reflect production behavior.

Full concurrency test suite: 6 passed (4 existing + 2 new), 0 failed.

---------

Co-authored-by: Test <test@test.com>

* feat(paging,#1222 PR-4): pressure-broker alert surface + ResourcePool→PressureSource adapter (#1245)

Closes the action-surface gap on Joel's "memory must be FULLY managed"
directive (2026-05-14). After PR-3 the broker can ACT on Docker
pressure; PR-4 makes it TELL operators what it did, AND brings the
DockerTierPool into the broker via a clean adapter rather than forcing
a duplicate trait implementation.

## Two pieces

### 1. ResourcePoolAdapter (paging/adapter.rs, 248 LOC, 9 tests)

Bridges Arc<dyn ResourcePool> → impl PressureSource. Required because
ResourcePool (sibling's #1228 — used by DockerTierPool, future HF
cache, future system-RAM tier) and PressureSource (Phase 7 broker
trait) are parallel traits covering the same conceptual ground.
DockerTierPool only implements ResourcePool, so it couldn't register
with the broker at all.

Derivation rules (all tested):
- pressure() = usage_bytes / capacity_bytes; 0 when capacity==0 (tier
  not under management — broker neither alerts nor acts on it)
- evict_some() forwards to evict_at_least(want); want = max(overshoot,
  10% of capacity) so a pool at exactly 100% gets a non-zero request
- stats_snapshot() derives PoolStats; hit/miss/eviction/inflight
  default to 0 since ResourcePool doesn't expose them (broker uses
  pressure + name for decisions; rest is diagnostics)

Filed follow-up issue tracking the trait-unification cleanup per Joel
"code concurrency ONCE then incorporate it" — adapter is the safe NOW
move; collapsing the two traits is the right LATER move.

### 2. PressureAlert + sink wiring (paging/broker.rs)

- New typed PressureAlert (ts-rs export, camelCase wire format) with
  tier_name, pressure, tier label, bytes_freed, action_taken, at_ms
- AlertSink type alias (Arc<dyn Fn(PressureAlert) + Send + Sync>)
- PressureBroker::add_alert_sink() — register as many sinks as needed
- emit_alert() — WARN log + every registered sink, called from
  relieve() once per pool the broker tried to relieve
- PressureTier::label() — canonical lowercase strings for IPC
- Closes the TODO at line 311 of broker.rs ("Future: emit IPC event or
  log when triggered=true")

## Why "even with 0 bytes freed" matters

Alert fires with action_taken=true even when evict_some returns 0
(fully-pinned pool, docker daemon down, etc). Zero-byte alert IS the
signal "this tier is hot AND stuck" — operator needs that distinct
from no alert. ReliefReport.triggered stays false in that case
(matches existing semantics: triggered tracks bytes-freed action),
but the alert surfaces.

## Tests (24 total, all green)

paging::adapter: 9 tests covering pressure derivation, capacity==0
short-circuits, evict_some 10% floor, overshoot semantics,
forwarding, dyn-dispatch via PressureSource trait object.

paging::broker: 6 new tests on top of existing 9 — alert per acted
pool, alerts per over-budget pool in critical, no alerts below
threshold, zero-byte alert when pool can't evict, PressureTier label
canonical strings, PressureAlert serde camelCase round-trip. Existing
broker tests pass unchanged (alert emission is additive — does not
alter triggered/bytes_freed semantics).

Clippy at baseline 162 (no drift). No unsafe, no async, no lock
nesting. parking_lot::RwLock for the new alert_sinks slot — same
discipline as everything else in the broker.

## #1222 status

- ✅ PR-1 (#1229): docker_tier discovery probe + paths::docker
- ✅ PR-2 (#1231): DockerTierPool impl ResourcePool (eviction stub)
- 📥 PR-3 (#1243): real evict_at_least via docker system prune
- 📥 PR-4 (this): pressure-broker alert surface + adapter

Operators can now subscribe to PressureAlerts via add_alert_sink to
forward into chat substrate / IPC / Grafana; until then alerts go to
the WARN log via runtime::logger("pressure-broker"). TS render layer
gets the typed wire format from shared/generated/paging/PressureAlert.ts.

Refs #1222.

Co-authored-by: Test <test@test.com>

* feat(modules,#1222 PR-3): real evict_at_least via docker system prune (#1243)

* refactor(persona): split evaluator.rs (1231 LOC) into focused submodules (#1208)

`persona/evaluator.rs` was a single 1231-LOC file mixing four
independent concerns: persona sleep state, per-room rate limiting,
post-inference adequacy check, and the `full_evaluate` gate
orchestrator. Split into:

  evaluator/mod.rs         (886 LOC) — gate orchestrator, FullEvaluate
                                       request/result types, 18 gate
                                       integration tests
  evaluator/sleep_state.rs (99 LOC)  — SleepMode + SleepState +
                                       2 unit tests
  evaluator/rate_limiter.rs (132 LOC) — RateLimiterState + RoomRateState
                                        + 2 unit tests (track_response,
                                        rate_limit_expired)
  evaluator/adequacy.rs    (207 LOC) — RecentResponse + AdequacyResult +
                                       check_response_adequacy + 7 tests

`persona/mod.rs` re-exports unchanged: `pub use evaluator::{...}` still
exposes SleepMode, SleepState, RateLimiterState, AdequacyResult,
RecentResponse, GateDetails, FullEvaluateRequest, FullEvaluateResult.
External callers see no API change.

Why these specific cuts:
- SleepState is reused independently anywhere a persona's voluntary
  attention state matters (not just Gate 1).
- RateLimiterState is a per-room cadence tracker that's a SIGNAL to the
  LLM, not a hard gate — independent of full_evaluate.
- Adequacy check is a separate phase (post-inference, not pre-response)
  that happens to share the file because it was written together.

Tests:
- `cargo test --features metal,accelerate persona::evaluator` →
  32 passed (every test from the original file, redistributed by domain).
- Full `persona::*` test suite → 451 passed, 0 failed, 3 ignored
  (no other module's imports broke).

Each new file < 250 LOC, mod.rs < 1000 LOC. Closes #1208 for the
`evaluator.rs` slice; `admission.rs` (1225) and `model_resolver.rs`
(1232) remain as separate cards.

* feat(modules,#1222): real evict_at_least via docker system prune (PR-3, stacked on #1231)

Stacks on PR-2 (#1231). Replaces the PR-2 stub (return 0) with real
two-stage eviction.

## Strategy

Two-stage escalation that frees only as much as needed:

1. **Soft (always tried first)**:
     docker system prune --force --filter "until=24h"

   Drops dangling images, stopped containers, unused networks older
   than 24h. Safe — does NOT touch in-use images, named volumes, or
   recent dev iteration artifacts. This is what a developer would
   manually run on a 'docker eats my disk' day.

2. **Aggressive (only if soft didn't free enough)**:
     docker system prune --force

   Same prune without the time filter. Frees ALL dangling artifacts
   regardless of age. Still does NOT touch in-use images or named
   volumes (Docker prune semantics).

Returns the actual bytes freed (sum across both stages), parsed from
Docker's stable 'Total reclaimed space: X.YYUNIT' summary line.
Returns 0 when docker isn't installed / daemon down / command fails —
broker treats as 'tier can't act, surface pressure to operator' (same
shape as DockerTierProbe::Unsupported).

## Parser

Standalone parse_reclaimed_bytes(output: &str) -> Option<u64>:

  - Handles all Docker units (B, kB, MB, GB, TB) with SI multipliers
    (1kB = 1000B per docker/cli convention)
  - Picks LAST 'Total reclaimed space:' line (Docker prints per-section
    totals during interactive runs; final line is the canonical total)
  - Returns None on missing line / unknown unit / unparseable number —
    distinct from Some(0) which means 'pruned successfully but nothing
    to free'

## Tests

8 tests pass (5 from PR-2 + 3 new):

  - parse_reclaimed_bytes_handles_all_units (B/kB/MB/GB/TB)
  - parse_reclaimed_bytes_returns_none_when_line_missing (5 malformed
    inputs — None vs Some(0) distinction)
  - parse_reclaimed_bytes_picks_last_summary_line (canonical-total
    semantics)

evict_at_least_never_panics replaces the PR-2 stub-asserting test.
Doesn't assert positive freed-bytes count because that requires a
live Docker daemon with prunable artifacts (flaky in CI). The unit
behavior is covered by the parser tests; live integration validation
happens during PR-4 chat-substrate alert work.

Clippy stays at baseline 162.

## Stacking

Base = feat/docker-tier-pool-impl-1222 (NOT canary). Once PR-2
(#1231) merges, GitHub auto-rebases this PR's base to canary and
the diff resolves to only PR-3 changes.

## Now-shipped under #1222

  - PR-1 (#1229): docker_tier discovery probe + paths::docker
  - PR-2 (#1231): DockerTierPool impl ResourcePool (eviction stub)
  - PR-3 (this): real evict_at_least via docker system prune
  - PR-4 (still open): chat-substrate alerts on >90% capacity

Joel directive 2026-05-14: 'memory in this system, including the
docker allotment needs to be managed by the system, FULLY.' With
PR-3, the system can actually ACT on Docker pressure (not just
report it). Closes the action gap.

Refs #1222.

---------

Co-authored-by: Test <test@test.com>

* fix(chat,#1159): URLCardAdapter HTML-escape every interpolation + safe-href guard (#1250)

Closes #1159 (PR-3 of #1100). Closes the URL-metadata-XSS surface that
PR-1 explicitly deferred (its doc comment named this slice).

## Vulnerability

URLCardAdapter.renderContent built the card HTML by string-interpolating
9 attacker-controlled fields without escaping:

- originalText (raw chat text — adversary types it)
- url (raw URL string — `"><script>...` works, `javascript:` works)
- title, description, siteName (async metadata fetch — server-attacker)
- favicon (constructed from domain — domain itself is parsed-safe but
  the slot was unescaped)
- domain (3 sites in template)

Two attack classes were live:
1. HTML injection — any of the 9 fields could break out of its quoted
   attribute or text context and inject `<script>`, `<img onerror>`, etc.
2. Scheme injection — the fallback-link `<a href="${url}">` accepted
   `javascript:`, `data:`, `vbscript:` URLs. A click executed in the
   page origin.

## Fix

1. **escapeHtml(s)** — same canonical 5-char escape used in
   TextMessageAdapter.escapeHtml. Safe in both text and double-quoted
   attribute contexts (escapes both `"` and `'`).

2. **safeHref(url)** — whitelist neutralizer. Returns `#` for any
   scheme outside the audit-once safe set (http, https, mailto, tel,
   ftp, sftp). Also passes protocol-relative `//` and same-document
   `#fragment` URLs as-is. Whitelist not blacklist because blacklists
   miss `\tjavascript:`, case mixing, `&NewLine;javascript:` HTML-entity
   smuggling, and any future code-executing scheme.

3. **Apply at every interpolation** in renderContent:
   - additionalText, url (×4 attribute sites + 1 anchor text),
     favicon, domain (×3), siteName, title, description → escapeHtml
   - href slot → safeHref then escapeHtml

## Tests

16 tests in tests/unit/url-card-adapter-xss.spec.ts, all pass.
Organized into four describe blocks (per-field escape, attribute-context
escape, href scheme neutralization, href whitelist preservation) so each
stays under the 80-line/function limit.

Each test asserts BOTH "raw injection string MUST NOT appear" and
"escaped form MUST appear" so a future bug regressing escape is
caught from both directions.

## Test-file naming + tsconfig discipline

- File is `.spec.ts` not `.test.ts` so it bypasses the
  `tsconfig.eslint.json` exclude `**/*.test.ts` (which would otherwise
  cause a parse-error and bump the ESLint baseline).
- Added the file to `tsconfig.eslint.json` include so it's parsed
  type-aware and lint-clean. Vitest discovers `.spec.ts` natively.
- This avoids the "no baseline bumps or parse-error debt for new test
  code" rule called out in the airc-8a5e direction broadcast 2026-05-14.

ESLint at 5461 baseline (no drift). TypeScript build clean.

## Path

PR-1 (#1154) — closed innerHTML Lit-reactivity hole, deferred metadata
XSS. PR-2 unrelated. PR-3 (this) — closes the deferred metadata XSS.
URLCardAdapter is now safe against the full audit set called out in
the original Joel review nit on #1154.

No behavior change for safe input. Single-encode contract preserved.

Refs #1100.

Co-authored-by: Test <test@test.com>

* refactor(persona): split admission.rs (1225 LOC) into mod + recipes (#1208) (#1251)

`persona/admission.rs` was 1225 LOC mixing the structural admission
gate, the IsMemorable trait, the v1 HeuristicIsMemorable recipe + its
policy tests, helpers, and the gate test suite. Split:

  admission/mod.rs     (985 LOC) — AdmissionGate::admit machinery,
                                    Candidate/Context/Config types,
                                    IsMemorable trait, envelope
                                    verification, seam recording, and
                                    the structural-gate test suite
                                    (replay, trust threshold, recipe
                                    error path, quarantine propagation,
                                    seam-emission invariants)
  admission/recipes.rs (326 LOC) — HeuristicIsMemorable struct + impl
                                    + 4 heuristic-policy tests
                                    (short_content, noise_phrase,
                                    duplicate, admit_synthesizes_engram)

`HeuristicIsMemorable` re-exported at the parent path via
`pub use recipes::HeuristicIsMemorable` — external callers see no API
change. Engram types previously imported privately from `super::engram`
are now re-exported `pub use` so submodules can reach them via `super::`.

Tests:
- `cargo check --features metal,accelerate -p continuum-core` clean.
- `cargo test --features metal,accelerate -p continuum-core --lib persona::admission`
  → 37 passed, 0 failed.

Closes #1208 — final slice. evaluator.rs done in #1242,
model_resolver.rs done in #1249, admission.rs done now.

Worktree-discipline note: this PR is the first work this session
authored from a proper `airc lane create` worktree rather than the
shared root checkout, after Joel called out that branch swaps in the
shared root were stomping uncommitted work.

Co-authored-by: Test <test@test.com>

* fix(chat-widget): empty state cleared by hidden attr — :host([hidden]) override (#1254)

Joel reported: chat widget's "Send your first message / Try @Helper..."
empty-state placeholder doesn't clear when the room actually has
messages. Visible after sending "my first message" into a room that
already had two prior messages — the empty-state panel still shows
below them.

## Root cause

`EmptyStateWidget` (LitElement, custom element `<empty-state>`) defines:

    :host {
      display: flex;
      ...
    }

ChatWidget toggles the empty state via the HTML `hidden` attribute
(updateEntityCount → emptyState.toggleAttribute('hidden', !isEmpty)).
The `hidden` attribute applies `display: none` via the user-agent
stylesheet — but the more-specific author rule `:host { display: flex }`
WINS the cascade, so `hidden` has zero visual effect. The toggle silently
no-ops; the panel keeps rendering.

This is the well-known custom-element-with-explicit-display gotcha
documented in the HTML5 spec:
https://html.spec.whatwg.org/multipage/interaction.html#the-hidden-attribute

## Fix

Add an explicit `:host([hidden]) { display: none; }` rule to the
component's static styles. Wins by being more specific than `:host`
alone (attribute selector wraps the host pseudo-class).

Other consumers of `<empty-state>` (UserListWidget, RoomListWidget,
TrainingDashboardWidget, the various Reactive* widgets) avoided this
bug by accident — they use `${this.isEmpty ? this.renderEmptyState()
: nothing}` to conditionally include the element rather than always-in-
DOM + toggle-hidden. ChatWidget chose the toggle-hidden pattern
deliberately because of CSS sibling rules around .messages-container,
so the right fix is to make `hidden` work as expected for the component.

## Verification

- `npm run build:ts` clean.
- Comment in code documents the gotcha + spec link so future readers
  understand why the rule is load-bearing (4 lines of CSS that look
  redundant alongside `:host { display: flex }` until you know the
  cascade history).

CSS-only behavioral fix: zero functional changes, no test added (UI
visual verification is the appropriate sign-off; will follow up after
merge with `npm start` + screenshot of a freshly-loaded populated room
showing no empty-state placeholder).

Co-authored-by: Test <test@test.com>

* feat(commands): add RustBackedCommand base + first refactor (#1198) (#1256)

Per Joel's "TS moves DOWN into rust… if not UI/UX it is rust" rule
(2026-05-14), every TS command in `src/commands/*` that exists only to
route into a Rust IPC handler does the same five things:

  1. Validate required params (throw ValidationError with consistent
     message + missing-field name)
  2. Resolve the Rust IPC client singleton
  3. Call the typed mixin method on the client
  4. Translate the snake_case Rust response to camelCase Result via
     `createXResultFromParams`
  5. Return the wrapped result

Steps 1, 2, and 5 were ~30 LOC of pure boilerplate per command. Steps 3
and 4 are the only variable bits. Pre-#1198 status quo: every command
re-wrote the boilerplate inline — exactly the uncompressed redundancy
the compression principle in CLAUDE.md exists to prevent.

This PR adds:

- `RustBackedCommand<TParams, TResult, TRest>` base class
  (`daemons/command-daemon/shared/RustBackedCommand.ts`):
  - Subclass declares `requiredParams` (which fields must be non-empty).
  - Subclass implements `callRust(params, client)` (the variable mixin
    call) and `toResult(raw, params)` (the variable result wrapping).
  - Base class owns: validation loop, client resolution, error
    consistency, the `execute()` orchestration.
  - `validateParams()` is overridable — subclasses needing richer shape
    constraints (e.g., typeof-object checks) call super then add their
    own.
  - `TRest` generic threads the raw mixin response shape through to
    `toResult` for type safety (no `unknown` cast at the seam).

- Canonical example refactor:
  `commands/cognition/admit-inbox-message/server/CognitionAdmitInboxMessageServerCommand.ts`.
  ~64 LOC → ~85 LOC, but most of the new lines are typed declarations
  (`requiredParams`, `AdmitInboxMessageRustResponse` type alias) that
  replace inline boilerplate. Every other command can adopt the same
  shape and lose ~30 LOC of envelope.

This is **PR-1**: pattern + one example. Other ~50 Rust-backed commands
adopt incrementally (don't churn-rewrite all in one PR).

Verification:
- `npm run build:ts` clean.
- The refactored command preserves the existing custom message-shape
  validation (typeof-object check) via the `validateParams` override
  pattern.

Closes #1198 for the pattern + first migration. Follow-ups can adopt
the base class one command at a time.

Co-authored-by: Test <test@test.com>

* fix(channel,#1253): default tick DB to SQLite handle (#1259)

* fix(channel,#1253): default tick DB to SQLite handle

* chore(clippy): lock warning baseline at 161

---------

Co-authored-by: Test <test@test.com>

* refactor(paging,#1246): collapse PressureSource into ResourcePool — single trait, drop adapter shim (#1264)

Closes #1246.

## Smell

`PressureSource` (broker.rs) and `ResourcePool` (pool.rs) were parallel
traits covering the same conceptual ground from two angles:

| Trait              | Method shape                                          |
|--------------------|-------------------------------------------------------|
| PressureSource     | name, pressure (0..1), evict_some, stats_snapshot     |
| ResourcePool       | tier_name, capacity_bytes, usage_bytes, evict_at_least, snapshot |

`PagedResourcePool` implemented both via two manual impls. Tier pools
that don't follow the per-key-page shape (DockerTierPool) only
implemented `ResourcePool` and needed a `ResourcePoolAdapter` shim
(#1245 PR-4) to plug into the broker. Two traits, one shape, an
adapter to bridge them — exactly the "code concurrency / control
surface ONCE then incorporate it" smell Joel flagged 2026-05-14.

## Fix

ResourcePool absorbs `pressure()` and `stats_snapshot()` as default
methods derived from the trait's existing core (capacity / usage /
snapshot). Tier impls override only when they have richer telemetry
(`PagedResourcePool` overrides `stats_snapshot()` to expose its
internal hit/miss/eviction counters; everyone else inherits the
defaults).

PressureBroker now holds `Arc<dyn ResourcePool>` directly. The broker
calls `evict_at_least(want)` instead of `evict_some()`, where `want`
is computed by a new `evict_amount_for(pool)` helper that aims to
drop pressure to `HEALTHY_TARGET_PRESSURE = 0.60` (matching the old
`evict_under_pressure()` "drop until healthy" behavior). 10%-of-cap
floor ensures non-zero ask even at exactly 100% pressure.

## Deletions

- `paging/adapter.rs` (ResourcePoolAdapter — vestigial after the
  collapse; every tier now plugs into the broker directly)
- `PressureSource` trait + `impl<K,V> PressureSource for PagedResourcePool`
  blanket impl — both replaced by direct ResourcePool consumption

## API change

External callers that registered `Arc<dyn PressureSource>` with the
broker now register `Arc<dyn ResourcePool>` instead — ergonomics are
the same (any tier implementing ResourcePool plugs in directly), the
trait name is the only churn. Currently no out-of-tree callers exist
besides the broker tests.

## Tests

66/66 paging tests pass: 9 broker tests (including the broker-end-to-end
on a real PagedResourcePool, the alert emission across the four states,
and PressureTier/PressureAlert serde round-trips), 57 pool tests.

The MockPool + StuckPool test fixtures got rewritten to implement
ResourcePool directly. MockPool's settable pressure path stays via a
`pressure()` override; capacity/usage are synthetic so the broker's
`evict_amount_for` produces sane requests.

## Diff stats

- `paging/pool.rs`: +44/−6 (default methods on ResourcePool + override
  on PagedResourcePool's stats_snapshot)
- `paging/broker.rs`: heavy rewrite (PressureSource → ResourcePool,
  evict_some → evict_at_least + evict_amount_for, mock fixtures
  rewritten)
- `paging/mod.rs`: drops adapter export, drops PressureSource export
- `paging/adapter.rs`: deleted (252 LOC removed)
- `modules/cognition.rs`: comment updated (PressureSource → ResourcePool)

## Clippy / baseline

Clippy at 161 (was 162). The deleted adapter shed one warning.

## Why this matters

Tier pools (Docker, KV cache, future HF cache, future system-RAM,
future NVMe) now plug into the pressure broker via the SAME trait
they already implement for capacity reporting. No more "do I need a
shim?" question. The compression rule from CLAUDE.md applies:
"For ANY decision (logic or data), can you point to exactly ONE place
in the codebase?" — for "tier capacity + eviction + pressure", the
answer is now ResourcePool, period.

Co-authored-by: Test <test@test.com>

* perf(coordination,#1260): track room activity for temperature decay (#1263)

Co-authored-by: Test <test@test.com>

* refactor(commands,#1198): migrate recall-engrams to RustBackedCommand (#1265)

Sister command of cognition/admit-inbox-message (refactored in #1256).
Same shape — validate, call mixin, wrap result — now expressed as
RustBackedCommand subclass declarations rather than re-implemented
boilerplate.

- requiredParams = ['personaId']
- validateParams() override adds the kind-companion required-field
  checks (by_id needs id, by_keyword needs keyword, by_origin needs
  origin); calls super first
- callRust delegates to the typed mixin
- toResult shapes the snake_case Rust response into the camelCase
  result via the existing factory

Behavior preserved: every original validation message + return shape
matches. Net: 86 -> 100 LOC, but most new lines are typed declarations
and the explicit per-field error messages — boilerplate is gone.

npm run build:ts clean.

Co-authored-by: Test <test@test.com>

* docs(architecture,#1266): codify recipe activity graph semantics (#1268)

Co-authored-by: Test <test@test.com>

* docs(grid,#1267): clarify airc pipeline and transport contracts (#1269)

Co-authored-by: Test <test@test.com>

* refactor(live,#1247): migrate livekit_agent per-key single-flight to ConcurrencyPolicy (#1270)

Closes #1247.

## Smell

`live/transport/livekit_agent.rs:86` declared a hand-rolled per-key
lock map:

```rust
static AGENT_CREATION_LOCKS: std::sync::Mutex<
    Option<std::collections::HashMap<(String, String), Arc<tokio::sync::Mutex<()>>>>,
> = std::sync::Mutex::new(None);
```

Used by `get_or_create_agent` to gate concurrent creation of the same
(call_id, user_id) — TOCTOU prevention against 3 concurrent callers all
calling `connect()` and creating 3 redundant agents+video loops.

Two problems:

1. **Reimplements `ConcurrencyPolicy`** — the substrate already shipped
   the canonical primitive in #1230 (TokioConcurrencyPolicy single_flight),
   hardened with panic-safe Drop guards (#1232) + refcount-per-key
   cleanup (#1235). The livekit code carried the exact bug class
   the substrate already solved.

2. **Lock-map entries leaked** — the prior code only released a per-key
   lock entry inside `remove_agent`. Transient agents that errored on
   `connect()` (network blip, LiveKit down) never reached `remove_agent`,
   so their lock-map entries lived forever. ConcurrencyPolicy's
   refcount drops in-flight slots automatically when the last awaiter
   completes, regardless of whether the work succeeded or panicked.

## Fix

Replace `AGENT_CREATION_LOCKS` with a module-level OnceLock holding
`Arc<TokioConcurrencyPolicy<(String, String), Arc<LiveKitAgent>, String>>`.

`get_or_create_agent`:
- Fast path unchanged: `agents.read()` lookup, return early if found.
- Slow path: construct an async work closure that does the post-policy
  re-check + `LiveKitAgent::connect()` + agents map insert + video loop
  spawn, then call `policy.single_flight(key, work)`.
- Concurrent callers for the same key all await the SAME Shared future
  the policy returns, so `connect()` runs ONCE and the result is
  broadcast to every caller.

`remove_agent`:
- No more lock-map cleanup — the policy self-evicts in-flight slots
  via refcount. `remove_agent` only owns the steady-state agents map
  now (drop the agent, disconnect from LiveKit room).

## Validation

- `cargo build --features metal,accelerate` — clean
- `cargo test live::transport --features metal,accelerate` → 16/16 pass
- `cargo clippy --features metal,accelerate` — 161 warnings (was 162;
  the deleted `#[allow(clippy::type_complexity)]` block shed one).

## Net diff

- ~50 lines removed (the static + lock-map cleanup in remove_agent)
- ~50 lines added (the module-level policy + work-closure shape in
  get_or_create_agent)
- Zero behavior change for the steady state; meaningful improvement
  for the transient-agent leak case.

## Architectural alignment

This is the second migration onto ConcurrencyPolicy after the
analyzer's adoption that already lives in canary. Same primitive, same
guarantees. Joel directive 2026-05-14 'code concurrency ONCE then
incorporate it' — livekit was the last per-key single-flight
reimplementation in the codebase that I'd flagged in #1247.

Co-authored-by: Test <test@test.com>

* fix(config): make SQLite the default main database (#1271)

* fix(config): make sqlite the default main database

* chore: lower eslint baseline

* chore: lower eslint baseline after canary merge

* chore: sync generated cognition bindings

---------

Co-authored-by: Test <test@test.com>

* fix(inference,#1262): delete dead compute_router.rs (#1277)

inference/compute_router.rs declared a CPU-vs-GPU dispatch policy that
sequential_always_cpu=true on Apple Silicon and routed any matmul under
500K flops to CPU. The file had ZERO callers anywhere in the crate (only
its own tests use ComputeRouter). Production hot path goes through
LlamaCppAdapter -> LlamaCppBackend -> llama.cpp Metal/CUDA which already
loud-fails on no-GPU per inference/model.rs:82
("CPU fallback is disabled.").

Carrying dead code that contradicts the no-CPU-fallback alpha contract
on paper but never executes is the same anti-pattern this card was
filed against. Delete to remove the misleading signal; if a future
tier-aware router is needed, build it then.

Audit findings + 3 sibling cards (#1273 verify+delete Candle qwen3.5,
#1274 delete metal_deltanet.rs, #1275 regression test) posted in
https://github.com/CambrianTech/continuum/issues/1262#issuecomment-4461757997.

Verified:
- cargo check --features metal: clean (0 errors, pre-existing warnings)
- cargo test --lib --features metal: 2092 passed, 0 failed

Lane: alpha flywheel #1272 lane 6.

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(airc): add realtime envelope contract (#1278)

Co-authored-by: Test <test@test.com>

* fix(inference,#1273): delete dead Candle Qwen3.5 GGUF backend (#1279)

Remove the Candle-side Qwen3.5 inference path (the hybrid DeltaNet +
Attention recurrence loop in vendored/quantized_qwen35.rs and its
ModelBackend wrapper in backends/qwen35_gguf.rs). 1100+ LOC removed.

Why it was dead:
- AIProviderModule::register_adapters (modules/ai_provider.rs:221) only
  registers LlamaCppAdapter for local inference. CandleAdapter is
  imported but never instantiated.
- Qwen35GgufBackend was only reachable via backends::load_gguf_backend,
  whose only callers were unregistered (CandleAdapter, ContinuumModel,
  bin/* utilities) — none in the production hot path.
- Production Qwen3.5 chat goes through llama.cpp (vendored,
  statically linked) via LlamaCppAdapter → LlamaCppBackend.

Scope-down from initial #1273 plan:
The original plan was to delete the entire Candle inference chain
(CandleAdapter, ContinuumModel, quantized.rs, vendored qwen2/llama
backends). cargo check confirmed broader scope is entangled with
plasticity LoRA training tests, which use compact_llama_safetensors
+ rebuild_with_stacked_lora. That broader deletion needs a separate
audit of plasticity's production reachability and is deferred to a
follow-up card.

This PR keeps everything plasticity touches (model.rs,
candle_adapter.rs, quantized.rs, llama_safetensors.rs,
compact_llama_safetensors.rs, vendored qwen2/llama) and only deletes
the qwen3.5-specific Candle path that has no plasticity dependency.

Wire change:
- backends::load_gguf_backend now returns a typed error for
  "qwen3"|"qwen35" architectures pointing callers at LlamaCppAdapter,
  rather than silently dispatching to the deleted Candle backend.

Verified:
- cargo check --features metal: clean (0 errors, 61 pre-existing warnings)
- cargo test --lib --features metal: 2096 passed, 0 failed (4 more than
  baseline — vendored qwen35 module registration removed some dead-code
  warnings that were eating test discovery)

Lane: alpha flywheel #1272 lane 6.
Audit context: https://github.com/CambrianTech/continuum/issues/1262#issuecomment-4461757997
Verification: https://github.com/CambrianTech/continuum/issues/1273#issuecomment-4461839438

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(inference,#1274): delete dead vendored/metal_deltanet.rs (+ shader) (#1281)

`vendored/metal_deltanet.rs` was a stub for a never-wired Metal kernel.
Its sole "implementation" was `bail!("Metal DeltaNet kernel not yet
wired — use CPU path")` plus a doc comment "Returns Err to signal the
caller to fall back to CPU." Greps confirm zero callers anywhere
(only `vendored/mod.rs:8` declared the module).

Also delete the companion shader `vendored/deltanet_recurrence.metal`
which had no remaining call site after removing the stub Rust
function.

Carrying a "fall back to CPU" pattern in code that nothing reaches is
the same anti-pattern this card was filed against (#1262 audit).

Verified:
- cargo check --features metal: clean
- cargo test --lib --features metal: 2096 passed, 0 failed

Lane: alpha flywheel #1272 lane 6.
Audit: https://github.com/CambrianTech/continuum/issues/1262#issuecomment-4461757997

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(inference,#1275): regression test for no-CPU-fallback alpha contract (#1282)

Add `tests/no_cpu_fallback_contract.rs` — three forbidden-strings
ratchets that fail the build if a future PR weakens the
no-CPU-fallback contract:

1. `select_best_device_panics_loudly_on_no_gpu` — asserts
   `inference/model.rs::select_best_device` keeps the
   `panic!("No GPU device available for inference. CPU fallback is
   disabled.")` loud-fail and tries CUDA + Metal before panicking.

2. `ort_providers_documents_no_cpu_fallback_contract` — asserts
   `ort_providers.rs` keeps the "CPU fallback is forbidden" comment
   that documents the rule from source.

3. `llamacpp_adapter_uses_loud_fail_for_no_local_model` — asserts
   `LlamaCppAdapter` uses the typed `NoLocalModelLoadable` error
   (shipped in #1093 / lane A PR-2) rather than a silent skip.

Pattern: same forbidden-strings ratchet shape as lane F PR-2 (#1129
TS persona forbidden-strings), applied to the Rust inference layer.
A test failure points the future-PR-author at the exact contract
they're about to weaken.

Closes the acceptance criterion #3 of #1262 ("regression test per
fallback path"). Final PR (4 of 4) for the silent CPU fallback audit.

Verified:
- cargo test --features metal --test no_cpu_fallback_contract:
  3 passed, 0 failed

Lane: alpha flywheel #1272 lane 6.
Audit: https://github.com/CambrianTech/continuum/issues/1262#issuecomment-4461757997

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(airc): add realtime replay adapter (#1283)

* feat(airc): add realtime replay adapter

* chore: ratchet clippy baseline

---------

Co-authored-by: Test <test@test.com>

* fix(scripts,#1257): add cargo-test.sh wrapper that auto-applies platform GPU features (#1285)

Make the obvious developer command work on every platform without
requiring contributors to memorize the per-platform Cargo feature
incantation.

Before:
  cd workers/continuum-core && cargo test tick_db_handle --lib
  → fails in vendored llama crate; "metal" or "cuda" feature required

After:
  ./scripts/cargo-test.sh tick_db_handle --lib    (anywhere from src/)
  npm run test:rust -- tick_db_handle --lib
  → auto-detects platform, appends --features metal,accelerate (Mac) /
    --features cuda,load-dynamic-ort (Linux+Nvidia) / etc.

Implementation:
- `scripts/cargo-test.sh` sources the existing
  `scripts/shared/cargo-features.sh` detector (single source of truth
  for platform→features, also used by build-with-loud-failure.sh and
  git-prepush.sh) and forwards arbitrary args to `cargo test`.
- `npm run test:rust` alias added next to `test:precommit` /
  `test:prepush` for discoverability.
- `workers/continuum-core/TESTING.md` documents the friction, the
  wrapper, the CARGO_TEST_NO_FEATURES escape hatch (for verifying the
  loud-fail guard itself), and the relationship to the other test
  entry points.

The wrapper does NOT weaken the no-CPU-fallback compile guard — it
just spares the dev from typing the platform-correct features every
time. The guard still fires in CARGO_TEST_NO_FEATURES=1 mode.

Verified:
- ./src/scripts/cargo-test.sh --test generated_barrel_sync → 8 passed,
  0 failed (8.5s, used --features metal,accelerate on this Mac).

Closes #1257.

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(chat,#1158): DRY adapter base default renderMessageElement (#1189)

* refactor(chat,#1158): lift renderMessageElement default into AbstractMessageAdapter

Three adapters (TextMessageAdapter, URLCardAdapter, ToolOutputAdapter) had
byte-identical override bodies of the form: parseContent, createAdapterWrapper,
renderContent, template.innerHTML, wrapper.appendChild(fragment).

That is now the default body of AbstractMessageAdapter.renderMessageElement.
The overrides are deleted; the live message-content slot still never sees
innerHTML (the parse happens on a detached template), and Lit-managed
reactive children inside the message bubble keep their state.

ImageMessageAdapter retains its custom override -- it builds img nodes via
property assignment to keep src and alt out of any HTML-parse path and does
not go through renderContent to string.

Net minus 61 lines.

Closes #1158.

* chore(ratchet): lock in -2 eslint from #1158 adapter DRY lift

* chore(eslint-baseline): ratchet -2 from #1189 adapter base default lift

* chore(eslint-baseline): linux ratchet to 5459 (match macOS baseline)

Linux CI ratchet failed because eslint-baseline.linux.txt was still at
5461 while the macOS baseline (and current count on both platforms)
is 5459. The ratchet requires CURRENT == BASELINE strictly, so the
-2 improvement from #1189 needed to land in BOTH platform files.

Sibling: 8b51729f5 (chore(eslint-baseline): ratchet -2) updated
eslint-baseline.txt; this commit completes the platform symmetry.

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

* chore(eslint-baseline): re-ratchet -2 on both platforms after canary merge

After merging origin/canary into the branch, baselines (mac=5455,
linux=5456) need to drop by the #1189 deletion delta (-2) to
mac=5453, linux=5454. macOS verified locally by precommit:
"Current: 5453 errors". Linux value is +1 vs Mac per established
platform skew; CI will surface the exact number if it's off.

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

---------

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(precommit,#1186): chat-roundtrip persona-reply smoke test (#1199)

* feat(precommit,#1186): add chat-roundtrip persona-reply smoke test

Closes Joel beef: browser ping is pretty low bar (2026-05-14).

New test tests/precommit/chat-roundtrip.test.ts:
  1. Verifies at least one auto-responding user is seeded (catches BUG-105 family)
  2. Sends a unique probe via collaboration/chat/send into general
  3. Polls data/list collection=chat_messages with orderBy timestamp desc, limit 50
  4. Anchors on the probe by content match (sender-id and room captured)
  5. Asserts at least one reply appears in the same room, after the probe,
     from a different sender, with non-empty content

Wires into PRECOMMIT_TESTS so it runs alongside browser-ping. Window is 55s
to leave headroom under the 60s per-test cap that git-precommit.sh imposes.
Uses an explicit-question probe text because local personas filter
no-reply-needed messages aggressively (saves Metal cycles).

What this catches that browser-ping does not:
  - Cognition pipeline silently broken (the highest-value catch)
  - chat-send rejecting the probe (room missing, attribution broken)
  - Persona seed step regressed (no AI users to reply)
  - chat_messages write path broken

Validated live: Helper AI replied to the probe in 5s on a clean stack.
Repeated back-to-back runs can be slow due to Metal queue depth on local
inference; CI runs against a fresh stack and isn't affected.

Followups (sub-cards):
  - 1186 PR-2: path-tier dispatcher (run heavy tests only when relevant
    paths touched). Wires on top of codex #1193 precommit-config loader.
  - 1186 PR-3: adapter unit tests when widgets/chat/adapters/ touched
  - Test reliability: clean local-inference queue between tests OR
    target a dedicated cloud persona for deterministic reply latency

Refs 1186.

* fix(precommit,#1186,#1199): wire chat-roundtrip into precommit-config.sh source of truth

Codex shipped #1193 adding scripts/precommit-config.sh as the canonical
source for PRECOMMIT_TESTS. My #1186 PR-1 (chat-roundtrip test) edited
the legacy defaults branch in git-precommit.sh, which only fires when
the config file is missing.

This commit updates precommit-config.sh to include chat-roundtrip
alongside browser-ping. The defaults branch is left in sync as
belt-and-suspenders so the gate works on either path.

Refs #1186, follow-up to codex #1193.

---------

Co-authored-by: Test <test@test.com>

* fix(inference,#1280): delete dead Candle adapter chain (Phase 1, ~2500 LOC) (#1288)

Per the plasticity reachability audit on #1280
(https://github.com/CambrianTech/continuum/issues/1280#issuecomment-4462181316),
production routes local inference exclusively through `LlamaCppAdapter`.
The Candle-side chain — `CandleAdapter`, `ContinuumModel`,
`select_best_device`, `load_model_by_id`, `quantized.rs::load_*_quantized`,
`backends::generate`, `backends::load_gguf_backend` — was reachable only
through itself or orphaned `bin/*` files. Plasticity's IPC handlers
(`plasticity/{analyze,compact,compress,topology,pipeline}`) work on
safetensors files via plasticity's own helpers and don't touch this
chain.

Deleted:
- `inference/candle_adapter.rs` (1486 LOC)
- `inference/quantized.rs` (287 LOC)
- `inference/model.rs` collapsed from 857 → 167 LOC, retaining only
  `rebuild_with_stacked_lora` (used by `backends/llama_safetensors.rs::CompactLlamaSafetensorsBackend`,
  test-only, slated for Phase 2 deletion alongside the safetensors
  backends once plasticity LoRA training is migrated or retired)

Wire updates:
- `ai/mod.rs`: drop `pub use crate::inference::CandleAdapter` re-export
- `inference/mod.rs`: drop `candle_adapter`/`quantized` modules + their
  re-exports; keep `model::rebuild_with_stacked_lora` only
- `modules/ai_provider.rs`: drop dead `CandleAdapter` import (it was
  imported but never instantiated by `register_adapters`)

Contract relocation (the audit's flagged risk):
The no-CPU-fallback `panic!("...CPU fallback is disabled")` in
`select_best_device` was deleted along with the rest of the dead chain.
The contract's actual production enforcement was already on llama.cpp:
`LlamaCppConfig::default()` sets `n_gpu_layers: -1` (= "all layers on
GPU"), and llama.cpp's loader hard-fails when no GPU is available.
`tests/no_cpu_fallback_contract.rs` is updated atomically to assert the
`n_gpu_layers: -1` invariant in `backends/llamacpp.rs` rather than the
deleted panic site. The `ort_providers` and `LlamaCppAdapter` assertions
survive unchanged.

Net: 7 files changed, +92 / -2546 LOC.

Verified:
- cargo check --features metal: clean (52 pre-existing warnings, 0 errors)
- cargo test --test no_cpu_fallback_contract: 3 passed (new contract
  assertion `llamacpp_default_config_requires_full_gpu_offload` green)
- cargo test --lib --features metal: 2166 passed, 0 failed

Phase 2 (deferred): delete safetensors backends + vendored
qwen2/llama backends + `rebuild_with_stacked_lora` once plasticity's
production reachability allows.

Audit: https://github.com/CambrianTech/continuum/issues/1262#issuecomment-4461757997
Mission: Joel 2026-05-15 — "eliminate slop and slowly oxidize this project"

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(chat): add AIRC migration inventory gates (#1296)

* fix(config): make sqlite the default main database

* chore: lower eslint baseline

* chore: lower eslint baseline after canary merge

* chore: sync generated cognition bindings

* docs(chat): add airc migration inventory gates

---------

Co-authored-by: Test <test@test.com>

* fix(chat,#1260): track room activity for temperature decay (#1302)

* fix(chat,#1260): track room activity for temperature decay

* chore(lint): ratchet eslint baseline

* chore(lint): ratchet linux eslint baseline

---------

Co-authored-by: Test <test@test.com>

* fix(install,#1035): create continuum-core socket directory (#1304)

Co-authored-by: Test <test@test.com>

* fix(ci,#1035): clear production npm audit gate (#1305)

Co-authored-by: Test <test@test.com>

* refactor(cognition,#1295): generate_recipe PR-1 — pure-functions slice in Rust (#1298)

First slice of RecipeGenerateServerCommand.ts (371 LOC) → Rust per the
oxidization mission (#1248 umbrella). Same shape as #1289 (rate_proposals):
pure-functions slice first, IPC handler in PR-2, TS shim collapse in PR-3.

Per the carrier-types design block on #1295: the runtime registry state
that the TS prompt depends on (TemplateRegistry.list output, existing
recipe IDs from RecipeLoader.getInstance().getAllRecipes()) crosses the
IPC boundary as explicit RecipeGenerationRequest fields. Keeps the
prompt builder + validator pure, testable, and parity-checkable.

What's in this PR (4 modules, 40 tests):

- types.rs (5 ts-rs exports)
  - RecipeTemplateInfo, RecipeGenerateHints, RecipeGenerationRequest,
    RecipeGenerationResponse, RecipeDefinitionShape
  - All camelCase serde + ts-rs auto-export to shared/generated/cognition/
  - 5 round-trip / shape-acceptance tests

- prompt.rs (build_recipe_system_prompt + build_recipe_user_prompt)
  - System prompt mirrors TS buildSystemPrompt byte-for-byte (schema
    block, available-templates list, standard-pipeline pattern, rules)
  - User prompt mirrors TS buildUserPrompt (description + optional hints
    rendered as bulleted "Hints:" block)
  - 8 tests covering anchors, template rendering with 0/N entries, all
    hint types, partial hints, empty-hints skip-block

- parser.rs (parse_recipe_from_ai_response → RecipeDefinitionShape)
  - Same regex anchor as TS: /\{[\s\S]*\}/ extracts JSON envelope
  - Tolerates prose preamble + markdown fences (matches TS behavior)
  - Typed ParseError::NoJsonEnvelope / MalformedJson with raw_preview
    capped at 500 chars (mirrors TS slice(0, 500))
  - 7 tests covering happy-path + prose preamble + fence + no-JSON +
    malformed + unknown-fields-tolerated + missing-optionals + cap

- validator.rs (validate_recipe_structure → Vec<String>)
  - Mirrors TS validateRecipe checks: required fields, kebab-case
    uniqueId, pipeline shape, RAG template messageHistory, strategy
    enum + required arrays, role type + requires
  - In-request duplicate check via existing_recipe_ids carrier
  - Filesystem collision check + sentinel-template existence stay
    TS-side (PR-3 shim) — they're pure FS / runtime-registry concerns
  - 12 tests covering happy path, every required-field gap, kebab-case
    rejection, empty pipeline, malformed steps, invalid enums, missing
    strategy arrays, role schema, in-request duplicate

## Why no fallback

Per #1262, the TS path's silent error-on-malformed-JSON returns
{ success: false, error: '...' }. Rust returns typed Err — PR-2 IPC
handler maps it to validationErrors[] for the JTAG envelope.

## Next

- PR-2: cognition/generate-recipe IPC command wiring
  AIProviderRegistry::generate_text + the prompt+parser+validator
- PR-3: RecipeGenerateServerCommand.ts becomes thin shim that gathers
  templates + existing recipe IDs, calls Rust, FS collision-checks +
  saves on success

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cognition,#1289): rate_proposals PR-1 — pure-functions slice in Rust (#1290)

First slice of ProposalRatingAdapter.ts (252 LOC TS) → Rust per the
oxidization mission (#1248 umbrella). Pure-functions-first: types +
prompt builder + parser shipped without IPC wiring or AI integration,
so behavior parity is testable before the IPC layer lands in PR-2.

What's in this PR:
- cognition/rate_proposals/types.rs: RatingMessage, ResponseProposal,
  RatingContext, ProposalRating with serde camelCase + ts-rs auto-export
  to shared/generated/cognition/
- cognition/rate_proposals/prompt.rs: build_rating_prompt mirrors TS
  buildRatingPrompt byte-for-byte (header, conversation history,
  proposals with index/proposer/confidence, rating criteria, output
  format anchors, behavior nudges)
- cognition/rate_proposals/parser.rs: parse_ratings_from_ai_response
  with ParseConfig defaults; regex anchors mirror TS exactly (same
  case-insensitive splits, same [0-9.]+ score capture that drops
  leading minus, same Reasoning: blank-line termination)

25/25 tests pass. ts-rs exports the four wire types so the TS shim in
PR-3 can import generated definitions instead of hand-writing duplicates.

Next:
- PR-2: cognition/rate-proposals IPC handler wiring
  AIProviderRegistry::select + adapter.generate_text to the prompt+parser
  shipped here
- PR-3: ProposalRatingAdapter.ts collapses to thin
  Commands.execute('cognition/rate-proposals', ...) shim

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resources,#1239): Phase 1 — system/docker-tier-stats IPC + ts-rs DockerTierStats (#1297)

Per Joel's "keep finding work" / mission to surface substrate pressure
to operators. The audit on #1239
(https://github.com/CambrianTech/continuum/issues/1239#issuecomment-4464969871)
found the gap is bigger than the card text suggests: PressureBroker is
built but never instantiated, DockerTierPool never registered,
continuum status is bash-side. Phasing the work so Phase 1 surfaces
the data without the missing broker singleton.

Phase 1 (this PR):
- New `system/docker-tier-stats` IPC handler in `SystemResourceModule`
  calling `DockerTierPool::snapshot_stats()` (new convenience method,
  one probe per call) — returns typed `DockerTierStats`
  (capacityBytes, usedBytes, pressure, detected).
- ts-rs export at `shared/generated/resources/DockerTierStats.ts`.
- IPC mixin entry `dockerTierStats()` on the RustCoreIPC client.
- TS server command at `commands/system/docker-tier-stats/` (generated
  via standard CommandGenerator + spec, then refactored to a thin
  rustClient.dockerTierStats() pass-through matching the
  SystemResourcesServerCommand pattern).
- Unit test asserts the IPC always returns the expected shape
  regardless of whether Docker is installed (CI passes without).
- Clippy baseline ratcheted -11 (157 → 146) — incidental cleanup.

Phase 2 (separate card): bootstrap PressureBroker singleton at server
startup, register DockerTierPool + future tiers, run the relief tick,
add chat-substrate alert sink so >90% surfaces as a chat message.

Phase 3 (separate card): typed `ResourceError::DiskCapacity` refusal at
production hot paths (model pull, container start, image build, gguf
download).

Verified:
- cargo test --lib --features metal docker_tier: 15 passed
- npx tsc --noEmit -p tsconfig.json: clean
- ESLint baseline holds at 5452

Mission: Joel 2026-05-15 — "eliminate slop and slowly oxidize"

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cognition,#1289): rate_proposals PR-2 — IPC handler + orchestrator (#1291)

Wires the prompt+parser shipped in PR-1 (#1290) to AIProviderRegistry::
generate_text via the cognition/rate-proposals IPC command. Stacked on
PR-1 (rebase to canary once PR-1 merges).

Same architecture as cognition/should-respond shipped today by codex on
#1284 (oxidizer pattern: native-truth Rust core, thin TS shim collapses
in PR-3). Shares the AIProviderRegistry singleton with shared_analysis,
so concurrent rater calls go through the same registry read-lock — no
new contention surface.

What's in this PR:
- cognition/rate_proposals/orchestrator.rs — rate_proposals_with_ai()
  - Builds TextGenerationRequest with system+user messages
  - Calls global_registry().read().await + generate_text()
  - Parses response with parse_ratings_from_ai_response (PR-1 module)
  - Returns Vec<ProposalRating>
- RateProposalsRequest / RateProposalsResponse — ts-rs camelCase exports
  to shared/generated/cognition/ for the future TS shim binding
- modules/cognition.rs — new "cognition/rate-proposals" command branch
  delegating to the orchestrator
- 6 new tests (4 orchestrator + 2 ts-rs export bindings)

## Why no fallback

The TS createFallbackRatings helper that returns neutral 0.5 scores on
AI failure is NOT ported. It masks real provider outages and was caught
as a silent-success vector in the no-CPU-fallback audit (#1262). On
inference failure this returns Err — the chat substrate already handles
"no rater responded" by skipping peer-review for that round (no degraded
scoring path).

## Test plan
- cargo test cognition::rate_proposals — 31/31 pass (was 25 in PR-1, +6
  new orchestrator tests + ts-rs exports)
- cargo check --lib --features metal,accelerate — clean
- ts-rs emits shared/generated/cognition/RateProposalsRequest.ts and
  RateProposalsResponse.ts on cargo test (verified)

## Next: PR-3
ProposalRatingAdapter.ts (252 LOC) collapses to a thin
Commands.execute('cognition/rate-proposals', RateProposalsRequest) shim
binding against the generated TS types. ESLint baseline drops by the
deletion line count.

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cognition,#1295): generate_recipe PR-2 — IPC handler + orchestrator (#1301)

Wires the prompt+parser+validator shipped in PR-1 (#1298) to
AIProviderRegistry::generate_text via the cognition/generate-recipe IPC
command. Stacked on PR-1 (rebase to canary once PR-1 merges).

Same shape as #1289 PR-2 (rate_proposals IPC). Shares the
AIProviderRegistry singleton with shared_analysis + rate_proposals,
so concurrent generator calls go through the same registry read-lock
— no new contention surface.

What's in this PR:

- cognition/generate_recipe/orchestrator.rs — generate_recipe_with_ai()
  - Builds system + user prompts via PR-1
  - Calls global_registry().read().await + generate_text() with
    Anthropic default + 0.4 temperature + 4000 max_tokens (matches
    TS RecipeGenerateServerCommand defaults exactly)
  - default_model_for_provider() mirrors TS switch lines 360-369
  - Parses with PR-1 parser; on parse failure returns Err with the
    typed ParseError as string
  - Applies unique_id_override AFTER parse, BEFORE validation
    (matches TS sequence at lines 80-82 / 85)
  - Runs PR-1 validator with carrier existing_recipe_ids
  - Returns { recipe, validationErrors }

- modules/cognition.rs — new "cognition/generate-recipe" command branch
  parsing { request, provider?, model?, temperature? } and delegating
  to the orchestrator

- 4 new orchestrator tests covering default-model parity, pinned
  generation constants, unique_id_override semantics

44/44 cognition::generate_recipe tests pass (was 40 in PR-1, +4 new).

## Why no fallback

Per #1262, the TS path returned { success: false, error: '...' } on AI
failure, masking provider outages. This Rust path returns typed Err on
inference failure — the JTAG shim in PR-3 maps it to a validationErrors[]
entry, preserving the failure mode for debugging.

## Validation errors NOT propagated as Err

Validation failures are returned in the response (not Err) so the shim
can render them via the JTAG envelope. Mirrors TS behavior exactly:
validationErrors go alongside the recipe; success: false reflects the
validation gate, not a parse failure.

## Next: PR-3

RecipeGenerateServerCommand.ts (371 LOC) becomes thin shim that:
- Gathers TemplateRegistry.list() + RecipeLoader.getInstance()
  .getAllRecipes().map(r => r.uniqueId) into RecipeGenerationRequest
- Calls Commands.execute('cognition/generate-recipe', { request, ... })
- On success path: FS collision check + sentinel-template existence
  check + saveRecipe + RecipeLoader.clearCache + reload

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cognition,#1295): generate_recipe PR-3 — collapse TS to thin shim (-220 LOC, -3 ESLint) (#1303)

* refactor(cognition,#1295): generate_recipe PR-3 — collapse TS to thin shim

RecipeGenerateServerCommand.ts goes from 371 LOC (owning prompt build,
AI dispatch, JSON parse, structural validation, FS I/O) to ~140 LOC
(JTAG framework + carrier-state gathering + post-Rust FS I/O only).

Per the oxidization mission (#1248 umbrella): everything that was
duplicating the Rust truth-layer is gone. Stacked on PR-2 (#1301).

What this PR does:

- Replace RecipeGenerateServerCommand.execute() body with:
  1. Validate JTAG `description` parameter
  2. Gather TemplateRegistry.list() + RecipeLoader.getInstance()
     .getAllRecipes() into the carrier RecipeGenerationRequest
  3. Commands.execute('cognition/generate-recipe', { request, ... })
  4. On post-Rust success: TS-side sentinel-template existence check
     (TemplateRegistry.has — runtime-registry state Rust can't see),
     saveRecipe to disk, RecipeLoader.clearCache + reload
  5. Map response → existing RecipeGenerateResult JTAG envelope

- Delete buildSystemPrompt() + buildUserPrompt() + parser + validator
  + defaultModelForProvider() (all moved to Rust in PR-1+PR-2).

- Regenerate shared/generated/cognition/index.ts barrel to export
  the 5 new ts-rs types (RecipeTemplateInfo, RecipeGenerateHints,
  RecipeGenerationRequest, RecipeGenerationResponse,
  RecipeDefinitionShape).

## Wire format

The IPC accepts a loose envelope { request, provider?, model?,
temperature? }. RecipeGenerationRequest carries availableTemplates
(from TemplateRegistry) + existingRecipeIds (from RecipeLoader) so
the Rust prompt builder + validator stay pure (no global state).

## What stays TS-side intentionally

- File I/O — JTAG framework concern, not cognition
- Sentinel-template existence check — runtime-registry state the
  Rust validator can't see; runs AFTER Rust validation so the error
  list is comprehensive
- RecipeLoader cache reload — persistence concern

## Test plan

- npm run build:ts: clean (post-shim collapse)
- 44/44 cognition::generate_recipe tests still pass (PR-1 + PR-2)
- Behavior parity preserved: same JTAG envelope shape, same default
  provider/model/temperature/maxTokens, same validation error format

Stacked on #1301 (PR-2). Will rebase to canary as PR-1 + PR-2 merge.

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

* chore(#1303): lock linux eslint baseline win

---------

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cognition,#1289): rate_proposals PR-3 — delete dead TS adapter (#1293)

ProposalRatingAdapter.ts (252 LOC) and its unit test (501 LOC) had ZERO
production callers — only the unit test imported the exported functions.
PeerReviewManager.ts (the actual peer-review pipeline) does NOT import
this adapter. So this is a clean DELETION, not a shim collapse.

Per the oxidization mission (Joel 2026-05-15): "(1) eliminate slop —
no half-finished work, no dead code, no parallel reimplementations."
A thin TS shim that nobody calls IS slop — Rust IPC handler shipped in
PR-2 (#1291) is the live truth; the cognition/rate-proposals command is
available to any future TS caller via Commands.execute with full ts-rs
typed bindings (RateProposalsRequest / RateProposalsResponse from #1290).

Originally PR-1/PR-2 commit messages said PR-3 would collapse the TS
adapter to a thin Commands.execute() shim. Investigation while drafting
this PR found zero production callers — `grep -rn "ProposalRatingAdapter\\
|rateProposalsWithAI\\|createFallbackRatings"` returns:
- ProposalRatingAdapter.ts (the file itself)
- ProposalRatingAdapter.test.ts (unit test, mocking AIProviderDaemon)
- nothing in PeerReviewManager.ts or any other production module
- nothing in chat substrate, persona response generator, or recipe path

A future TS caller wanting AI-driven proposal rating uses:
  Commands.execute<RateProposalsResponse>('cognition/rate-proposals', req)
with `req: RateProposalsRequest` from shared/generated/cognition/. No
intermediate shim layer adds value — it would just re-export the
already-typed primitive.

- Delete src/system/user/server/modules/cognition/ProposalRatingAdapter.ts
- Delete src/tests/unit/ProposalRatingAdapter.test.ts (the parsing +
  prompt-building behavior is now covered by 31 tests in Rust under
  workers/continuum-core/src/cognition/rate_proposals/)

- npm run build:ts — succeeded clean (no dangling imports)
- The 31 Rust tests under cognition::rate_proposals stay green (PR-1+PR-2)
- ESLint baseline drops by 753 LOC of dead TS

Stacked on #1291 (PR-2). Will rebase to canary when PR-1+PR-2 merge.

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* oxidizer: move AI gating decision to Rust (#1294)

* oxidize ai gating decision

* chore(#1294): lock gating eslint baseline win

---------

Co-authored-by: Test <test@test.com>

* feat(cognition,#1276): migrate VisionInferenceProvider to Rust cognition/vision-describe (#1292)

* feat(cognition,#1276): migrate VisionInferenceProvider to Rust cognition/vision-describe

Per Joel 2026-05-15 ("mission to eliminate slop and slowly oxidize this
project") and the #1248 oxidizer umbrella, move TS-side vision
inference orchestration to Rust. TS becomes a thin shim.

Outlier-validation pair with codex's #1284 (AIDecisionService.evaluateGating
→ cognition/should-respond, structured-decision shape); this card is
the freeform-shape outlier. Same Rust+thin-TS-shim pattern as
recall-engrams (#1265).

## Rust side (new)

`workers/continuum-core/src/cognition/vision_describe.rs` — 337 LOC.
Owns:
1. Vision-capable model selection (filter `model_registry` by
   `Capability::Vision`, prefer local providers). Single source of
   truth — no more `process.env.*_API_KEY` checks scattered in TS.
2. Prompt construction from option flags (detectObjects/Colors/Text,
   maxLength). Pure function; unit-tested.
3. Multimodal request assembly (text + base64 image content parts).
4. Inference dispatch via `runtime::execute_command_json("ai/generate",
   ...)` so the existing Rust adapters (Anthropic / OpenAI / LlamaCpp)
   shape the multimodal payload per their own native API contracts.
5. Response parsing into `VisionDescription`. Pure function; unit-tested.

ts-rs auto-emits `VisionDescribeRequest`, `VisionDescribeOptions`,
`VisionDescription` to `shared/generated/cognition/`.

## IPC wiring

`modules/cognition.rs` — adds the `cognition/vision-describe` handler
that parses params into `VisionDescribeRequest…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant