27B/MTP live-verified + a throughput baseline carries its window (#440/#441) - #2339
Merged
Conversation
… as text (#274/#396) Joel, 2026-08-13: "Always use uuid and never corrupt them with random prefixes" and "UUID's are NOT strings. If you're using strings for id you are writing slop." Both landed on code written minutes earlier, and the grep that followed found the same defect older and wider than my one file. Three fixes, in order of how badly they were wrong: 1. The four shipped recipe ids were hand-drawn patterns (c0de0001-0000-4000-...), a name wearing a UUID's costume — readable, collidable, and fake. Replaced with genuine v4s in both the authored JSON and the `shipped::` constants, written grouped (0xfed332c3_383c_45bb_...) so the two are checkable by eye. 2. comms::{MessageId, CorrelationId} were `String` newtypes. `MessageId::new("msg-1")` let any caller invent a namespace that collides with every other caller's. Both are now `Uuid`: MessageId::new() MINTS (no caller-supplied form exists), and CorrelationId::of_exchange(id) states the derivation the old `CorrelationId(id.0.clone())` left implicit. Still distinct TYPES — the compiler, not a naming convention, is what stops one being passed as the other. 3. EndpointId is deleted. Its values were `EndpointId::new("browser")` and `("rust-core")` — a client-kind label standing in for an identity, and with it the assumption that the web client is a distinguished endpoint. It is one client among many (mobile, SDK, TUI, another node's core). TransportEnvelope.source and .target are now PeerId, the substrate's one actor identity per identity/mod.rs. Zero callers outside comms/mod.rs; the stale generated binding goes too. Also closes the on-disk authoring hole the required `id` field opened: a recipe file that names no id now gets one DERIVED from its purpose (RFC 4122 v5 under a frozen namespace), so "author a file, zero code" keeps meaning zero code — no uuidgen — and every node that loads the same file agrees on its identity with nothing to reconcile. A recipe that DOES carry an id keeps it verbatim. Tests: comms 23 pass (envelope wire shape now asserts ids round-trip as their own UUIDs; minted_message_ids_are_unique pins the collision fix), experience 43 pass including the previously-failing an_experience_authored_on_disk_needs_no_rust. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…t, not the deploy path Joel, 2026-08-13: "Headless rust period. No need for node to run everything except for the web interface which is one of many, including mobile apps/sdk." — and then, because I kept treating the correction as a code-only matter: "Fix these severe misunderstandings ... regardless of where they are." This file is where the misunderstanding REPRODUCES. It is loaded into every agent session, and it opened with "EVERY TIME YOU EDIT CODE: Run `npm start` (MANDATORY)" under `cd src` — a directory that no longer exists — plus "ALL Rust binaries MUST be built via npm start". A fresh agent under amnesia reads that and concludes Node runs the system. It doesn't, and `continuum --help` has been saying so in its own first line the whole time: "build + run the headless Rust core". Rewrote the CRITICAL WORKFLOW section around what is actually true, and swept the other 11 Node-as-deploy claims scattered through the file: - The core is Rust and boots with no Node. Node builds the WEB desktop, which is one client among several (mobile, SDK, TUI, MCP, another node's core over the grid). Named the consequence, because it already cost us: a feature that lives in a client exists only for that client — how voice ended up web-only with every other citizen structurally mute (#58). Behaviour goes in the core; clients render. - The deploy path is `continuum reboot` (Rust build + relaunch + running-SHA verify), with `deploy-verify` and the version trio called out — that verification exists because a reboot once shipped a stale binary and reported success (#194). - `cargo build` stays discouraged for the RIGHT reason (a hand-built binary exists only on your machine, and a fresh clone must work with no manual steps, #291) — not the old implication that Rust must be built through npm. `cargo check` is named as the correct type-check-while-you-work tool, with the shared CARGO_TARGET_DIR. - `npm run build:ts` now says what it actually covers: the web client, and nothing about whether the core compiles. - Flagged `./jtag` inline as the legacy Node CLI. Left the invocations that follow, since their command NAMES are still accurate — it is the driver that is stale. No code change; the one surviving "npm start" is the sentence warning you off it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ed to a binary that does not exist
Joel: "Cu conflicts with some Unix program. Fix for all cases including windows"
and "It's a bug Claude" — it is, and worse than doc drift.
`cu` is /usr/bin/cu (UUCP call-unix) on every Unix. `uu` — the double-U of
contin-UU-m — is THE official short alias, and start-server.sh has installed
exactly that (plus a squatter guard) since 2026-08-01, with a comment naming this
very collision. Nothing in the tree installs `cu`. But the REFERENCES never
followed:
- Six benchmark harnesses defaulted to `~/.continuum/cache/cargo-target/{release,debug}/cu`.
Neither file exists — the only built CLI is `release/continuum`. So the default
resolved to nothing and every default-args run died at the first invocation.
matrix.py even carries a comment about this exact class of failure biting once
before (2026-07-22, stale debug-only default silently no-opping the sweep).
- Fixed by RESOLVING rather than renaming: a shared `_resolve_cli()` prefers what
is actually installed on PATH (`uu`, then `continuum`) and falls back to the
release build — so it works from a fresh clone, an installed box, or a dev tree,
on any platform, instead of hard-coding one machine's layout.
- Flag renamed `--cu` → `--uu` with all in-repo call sites updated
(sweep_all → matrix → headtohead → preflight_gpu chain).
Also swept 53 `cu <command>` occurrences in docs, Rust comments, and generated-TS
doc comments to `uu`. Left the ones that are ABOUT the collision (memory-bridge
README's "never bare `cu`", WAKEUP-AND-JOIN's rename note) — those are correct as
written, and legacy/ stays quarantined.
README dev section reworked in the same pass: the boot path is `continuum start` /
`continuum reboot` / `continuum ping`, with Node named as what it actually is —
the web client's build dependency, one client among mobile/SDK/TUI/MCP, not the
thing that runs the system.
Verified: all six harnesses compile (py_compile), cargo check clean, zero
`target/{release,debug}/cu` paths left in the tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ilently swallowing the job
Joel: "Fix all nodejs and python dependencies that are breaking our headless rust
core ... we must find all smell all the time and never ignore it."
AUDIT RESULT FIRST, because one half is good news:
Node spawns from the running core: ZERO. `Command::new("node"|"npm"|"npx"|
"deno"|"bun")` has no occurrences anywhere in continuum-core. The core is
genuinely headless Rust at runtime; Node exists only to build the web client.
Python spawns from the running core: FOUR sites, and neither kind was honest.
1. THREE were vestigial and were hiding untested code. `file_engine.rs` had three
`#[test]`s opening with `if Command::new("python3").arg("--version").output()
.is_err() { return; }` — a skip-guard from when the syntax gate shelled out to
`python3 -m py_compile`. That interpreter is GONE: the production path is
`code::syntax::validator_for` → `unbound_calls`, pure Rust. So on any box
without python3 — a CI runner, a fresh clone — three tests reported PASS while
asserting nothing at all. Guards removed; all 55 file_engine tests pass without
an interpreter present, which is the proof they never needed one.
Also corrected the doc on `introduced_undefined_calls`, which still told the
reader the analysis returns None when there is "no python". There is no python.
2. ONE is a REAL runtime dependency, and it was failing silently. `forge/start`
spawns `python3 <alloy_executor>` — a script that lives in the SIBLING
sentinel-ai repo, which a fresh clone of continuum does not have. When
`find_alloy_executor()` returned None the handler used pid 0 and wrote
`state: "queued"` — indistinguishable from a job legitimately waiting its turn.
So on every machine that had only cloned this repo, `forge/start` returned
SUCCESS for work that nothing would ever run. Now it fails loud, names the
missing script, says where to get it, and states plainly that the job was NOT
queued. The dependency itself is still there — excising it to Rust is #52/#99
— but it can no longer pretend to have worked.
The pattern in both: a Python dependency that had already been removed or had
never been satisfiable, still shaping behaviour through a stale guard and a
fallback. `[[fallbacks-are-illegal-fail-loud]]`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…, and the compiler now knows Joel: "UUID's are NOT strings … Well defined and named structs by reference must be used." Chasing that through persona_id turned up something sharper than the count. THE FINDING. `PersonaWorkspaceRegistry::resolve_persona` exists specifically to close what its own doc calls "the loose-`String` id boundary … the defect class that fed a dead id to a doomed eval." It has SEVEN call sites. SIX are its own tests. ONE is production (eval.rs). Against 55 `persona_id: String` fields. The check was correct and essentially nothing called it — a correct check nothing calls is nastier than a missing one, because it reads as covered. Nothing forced the call, because both sides were `String`. THE FIX — two types, one door: - `PersonaRef` (new, in identity/): what a CALLER writes — full UUID, 8-char short-id, or name. Explicitly NOT an identity. Its only accessor is `as_str()`; there is no `as_peer_id()`, because a name is ambiguous, mutable, and meaningless without a roster. - `PeerId` (existing canonical actor identity): what everything downstream holds. - `resolve_persona(&PersonaRef) -> Result<PeerId, _>` is now the ONLY bridge. Taking the newtype rather than `&str` is what makes resolution unskippable. `From<PeerId> for PersonaRef` exists (an identity is always a valid reference to itself); the reverse deliberately does not — it requires a roster. Wire shape is unchanged: `#[serde(transparent)]` over the same string callers already send, so no client, recipe, or stored payload changes. Short-id and name PX (#161) keeps working — that ergonomics is the whole reason a reference type has to exist separately rather than everything becoming a UUID. Converted, types pushed DOWN rather than laundered at the seam: - `CognitionEvalParams.persona_id` → `PersonaRef` - `restore_persona_workspace(&PersonaRef)` (was `&str`) - `append_failed_ledger(&PersonaRef, …)` (was `&str`) HELD, and stated rather than fudged: `CognitionEvalResult.persona_id` stays `String`. The struct derives `Default` across 21 fields and a persona reference has no sensible default — an empty one is a nonsense value that reads as a real answer. The real fix is splitting the fire-and-poll HANDLE from the completed RESULT (a handle knows only the requested ref; a result knows the resolved id), which is its own slice. Inventing a default to satisfy the type checker is the `unwrap_or` reflex: compiler quiet, runtime wrong. The reason is recorded at the field. Tests: persona_workspace 10, eval 22, identity 23 — all pass; full lib test build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… 21 sites Second slice of the id-typing migration. All ten `commands/memory/*` params plus the `MemoryManager` API they call now carry `PersonaRef` instead of `String`/`&str`: append_memory, append_event, load_corpus, has_corpus, get_corpus, multi_layer_recall, consciousness_context, persona_db_handle, hydrate_corpus_if_missing. Types went DOWN into the layer rather than being unwrapped at each call — the whole point of the previous slice. `.as_str()` now appears only where the value is genuinely being USED as text (a map key, a `starts_with` shape check, a directory handle), never to satisfy a signature one call later. Two `Default` derives removed (`ConsciousnessContextParams`, `LoadCorpusParams`) rather than giving `PersonaRef` a default. No caller used `::default()` on either, and an empty persona reference is a nonsense value that reads as a real answer — same reasoning as the eval result field held in the previous commit. What this makes visible, and does not yet fix: these commands still never RESOLVE. They accept a reference and hand it straight to the storage layer as a key, so a name or short-id reaches the DB unresolved. That was invisible while everything was `String`; it is now legible in the signatures. Wiring `resolve_persona` into the memory command path is the next slice (#164/#396). Tests: memory 219, rag 150 — all pass. Full lib test build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…rsona/*, cognition/observe
Third slice. Seven more command param structs stop typing a persona reference as
`String`: agent/solve, persona/identity/{get,set}, persona/instances/{get,despawn},
persona/wall/pin, cognition/observe (params + the `assemble` signature + its `Meta`,
so the type is consistent through the result rather than converted on the way out).
Same discipline as the memory slice: `.as_str()` appears only where the value is
being USED as text — `id_resolve::resolve` takes a `&str` by design because it also
serves rooms and cards — never to satisfy a signature.
Running total across the three slices: 39 persona params + the memory API + the
resolver itself. `persona_id: String` is down from 55 to 16 in the crate, and every
one that remains is an internal struct holding an already-resolved id (those want
`PeerId`, the next slice) rather than an unresolved caller reference.
Tests: full lib test build clean, commands suite passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…rce path carries it down Fourth slice. `CognitionTraceParams`, the four other introspect params, `CognitionReplayParams` + result, `RagComposeRequest`, and `DatasetFromTurnsParams` now carry `PersonaRef`. The RAG one went two levels deep rather than stopping at the param: `load_source`, `load_memory_source`, and `load_consciousness_source` all take `&PersonaRef` now, which deleted the two `&persona_id.into()` conversions the memory slice had left at those call sites. That is the shape to aim for — when the type reaches the bottom, the adapters in the middle disappear rather than accumulating. `persona_id: String` in continuum-core: 55 → 8. Every remaining one is an internal RECORD (memory/types, should_respond's AIDecisionContext, live/types, projection, shell_types, ai/types, sentinel) holding a value copied from a param. Those are deliberately NOT converted to `PeerId` yet. They hold whatever the caller sent, and nothing on those paths resolves — typing them as an identity today would assert something the code does not do, which is worse than leaving them `String`. They become `PeerId` in the same slice that wires `resolve_persona` into those paths, not before (#164/#396). Tests: replay 3, rag 18, full lib test build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…— the stub's premise expired `StubAircCitizen::subscribe_all_rooms` was an `unreachable!()` justified by a comment that said no test drives it. That was true when written and stopped being true at bf11a66 (#398 slice 3), which gave `PersonaSupervisor::materialize` a `subscribe_all_rooms` call to wire the doctrine/wall cache invalidators. Every supervisor test that materializes an adapter has been dying in that panic since — 5 of the 10, and they only surface on a FULL-suite run, which is how they sat unnoticed. Found while verifying my own id-typing slices: full suite came back 7073 passed / 5 failed, and the first question was whether I caused it. I did not — the diff of my five commits touches `commands/persona/*` only, never `persona/supervisor.rs` or `persona/airc_citizen.rs`, and `git log` on those two files points at #398. Fix: return `AircError::Transport` instead of panicking. This is NOT a fallback — the caller already handles that exact case explicitly (keeps both sources uncached, "correct, just slow", logs loud), so the tests now exercise the real degradation branch rather than aborting, and a stub still never pretends to hold a live stream. An empty stream WOULD have been the fallback: it would have looked like a working subscription that silently never invalidates. Comment rewritten to state what is true now, including why the old assertion was right when it was written. An assertion that outlives its premise is worse than no assertion — it reads as a guarantee. persona::supervisor: 10/10 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…e panic I removed
`stub_subscribe_panics_loudly` existed to prove the `unreachable!()` fired, so removing
that panic left the assertion failing for the right reason. Rewritten to pin what the
contract actually is now:
Err(Transport) — required
Err(anything else) — fail (the caller branches on Transport specifically)
Ok(stream) — fail LOUDEST, because that is the real fallback: a stub handing back a
stream looks like a live subscription that silently never invalidates
Matched rather than `expect_err`d because `FilteredEventStream` is not `Debug`.
FULL LIB SUITE NOW GREEN: 7078 passed, 0 failed, 42 ignored. Before this session's
last two commits it was 7073/5 — five supervisor tests panicking since #398 slice 3,
only visible on a full run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ARED, or the build fails Joel: "eliminate all smell or you will copy it." That is literally the mechanism — a model reading this tree learns its conventions FROM it, and `persona_id: String` was 55 sites teaching that ids are text. I did exactly that this session: minted `c0de0001-…` fake UUIDs because fake-looking ids were already normal here. Prose in CLAUDE.md does not stop that. A failing test does. Three tests in identity/mod.rs, running on every PR via the existing `cargo test -p continuum-core --lib` workflow (no skip pattern matches them): 1. `every_string_typed_identity_field_is_declared` — walks src/, finds any `<identity-name>_id: String | Option<String>` field, and fails unless it appears in LOOSE_IDS. Comments stripped first, so a doc line can never register as a field. The error names the file:field and tells the reader which typed form to reach for (`PeerId` for an actor, `PersonaRef` for an unresolved reference, a `*Id(Uuid)` newtype otherwise). 2. `no_declaration_outlives_its_defect` — a declaration whose field HAS been fixed fails too. Learned directly from `StubAircCitizen::subscribe_all_rooms`, whose comment stayed true-sounding for months after its premise expired and cost 5 silently-failing tests. 3. `declarations_carry_a_real_reason` — every entry starts with external:/pending:/ defect: and is longer than a shrug, the same bar the module-wiring audit (#344) holds. 73 declarations, honestly categorized: - **external** — LiveKit participant/room ids, log-envelope correlation fields. Another system owns the wire format. - **pending** — ours, but nothing on that path RESOLVES yet. Typing it as an identity today would assert something the code does not do. Converts in the slice that wires resolution (#164/#396). - **defect** — `peer_id: String` × 5. These ARE `PeerId`. I attempted the conversion in this session and reverted it: `PeerId` has no `JsonSchema` impl and the construction sites hold `&str`, so it needs its own slice rather than a rushed cascade. Declared as a defect so it stays visible instead of blending in. POSITIVE CONTROL, because a guard nobody has watched fail is the exact shape of defect I found earlier today: added `struct PositiveControlProbe { pub owner_id: String }`, confirmed the guard failed naming `identity/mod.rs: owner_id`, removed it, confirmed green. The guard also caught 12 sites my own inventory grep had missed — it is already strictly better than the method I was auditing with. Full lib suite: 7081 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…now --from-source
`start` shelled unconditionally into tools/scripts/start-server.sh, which runs a
full cargo build. A "governed" lifecycle verb was a wrapper around a bash file
that only exists inside a repo checkout (BigMama's find, 2026-08-13):
- a user holding ONLY the installed binary, with no source tree, could not
start a core at all;
- the CLI printed one line then went silent for the length of a compile —
called hung three separate times;
- the façade's honesty depended entirely on the script underneath.
Same class as the rest of the night's defects: a governed surface over a
hand-rolled path.
Now: `launch_core` locates the installed `continuum-core-server`
(CONTINUUM_CORE_SERVER override → next to the running exe → ~/.continuum/bin →
target/{release,debug} walking up) and execs it directly, keeping the existing
detach/log/pidfile handling unchanged. Building is an EXPLICIT request
(`continuum start --from-source`), never the silent default, and the
no-binary fallback says WHY it fell back and that it compiles first, rather
than going quiet for minutes.
The override refuses loudly when set but not a file — a wrong override must not
look like an absent one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…uard debt cleared) The working tree could not build tests. This finishes the identity-typing work that broke it and clears the guard debt it left behind. `cargo test -p continuum-core --lib --features metal,accelerate`: 7081 passed, 0 failed. PEER_ID IS A TYPE, NOT TEXT. The production conversions landed earlier; the test fixtures were left holding `&str`, so the crate compiled and the test target did not. Fixtures now derive a real UUID from the literal they used to carry (v5 under NAMESPACE_OID), which preserves every cross-site equality the tests depended on — "peer-a" in two places is still the same peer, it is just an id now instead of a name in costume. CAUGHT BY DOING IT: contracts/verification had the manifest keyed by a derived PeerId while the EVENT still claimed a raw string signer. The lookup is BY that signer, so converting one side made every verification test fail as MissingPeerManifest. One `test_peer_id` / `test_peer_str` pair now feeds both sides — the same shape of defect the newtype exists to prevent, reproduced in the fixtures while removing it from production. REAL HARDENING, not just fixture churn: `AircPeerManifest::validate` had DROPPED its empty-peer_id check on the theory that typing the field made it impossible. Typing killed `""`. It did NOT kill `Uuid::nil()`, which is still constructible and still means nobody — the type narrowed the hole rather than closing it. The guard is back, at the remaining expressible form. GUARD DEBT CLEARED: loose_id_guard's `no_declaration_outlives_its_defect` went red, correctly — four `peer_id` entries in LOOSE_IDS described fields that are now `PeerId`. Removed. The guard failing here is it working: a declaration list that can rot into a graveyard is worth nothing. Also includes the accumulated session tree: the Joel→Operator fixture sweep (no person's name hardcoded in test data), ts-rs regeneration, and rustfmt across the crate. That is why this touches ~717 files; the behavioural change is the identity typing and the nil-PeerId guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
`k3-serving` had no lifetime because it was named for a SUBSYSTEM, so it never died — a month-old durable subscription whose board reads were all corpses. Parted from both scopes. It also sat in TEST FIXTURES, which is the transmission vector: fixtures teach the next reader the convention, so a fixture naming a room after a subsystem teaches that rooms are named for subsystems. Renamed to `bench-swe-run-1` — an activity with a lifetime, which is what a room IS. The two remaining mentions in work.rs are the INCIDENT RECORD (4 of 12 cards there carried a 134h-expired lease) and are kept, now annotated RETIRED so nobody copies the naming from them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…zero — carry it on the wire and the board `SweGradeResult.error` documents its own contract: "a result with `error` is an ABSENCE, not a zero, and must never be tallied as a failed attempt." The grader earns that honestly — for the env class it re-runs the PRISTINE tree before declaring a fault, so a genuinely broken patch is never mislabelled. Then both consumers dropped the classification: - `benchmark.attempt.end` / `benchmark.autograde` published `resolved=false gate_ok=false` and nothing else. Every wire consumer (probe router → rooms, exam-room widgets, pulse monitors) reads that as a citizen who tried and lost. - `fold_run_card` read the RESULT's `infra_error` but never the GRADE's `error`, so the board folded `resolved: false` + phase `failed` for the same runs. The attempt loop already broke correctly on `g.error.is_some()` (attempts 2 and 3 were never burned) — the loss was purely in what got PUBLISHED, which is the part anything downstream can actually read. Measured on this box 2026-08-13: 8 of 36 distinct instances (14 of 91 receipts, 22%) grade UNGRADEABLE — requests, pylint, pytest and sympy. Every one of those zeros was indistinguishable from a capability failure on the wire, so the denominator of any rate computed off this stream was poisoned. Found by digging into sympy__sympy-11400: p2p 0/29 on the PRISTINE tree, i.e. the suite does not run in that environment at all. (#380/#383 own fixing the environments; this commit owns never again reporting their faults as scores.) - attempt.end + autograde now carry `ungradeable` + `grade_error` - `infra_error` takes the grade's error too — one field meaning "no valid verdict, and why", fed by both sources rather than a second parallel field - `resolved` returns to `None` when ungradeable — the same "no verdict" a pre-grade card carries, because that is the truth - new phase `ungradeable`, ordered ahead of `failed` (a run can carry both a failed marker and an ungradeable grade; the absence is the truer of the two) Test asserts absence-not-zero on the real sympy-11400 shape, with a positive control (same shape, no grade error) that must still fold as a capability zero — so the test cannot pass by simply never reporting failure. This is the #384/#386 class one layer up: those classified INFRA at the solve level, this classifies it at the GRADE level and gets it onto the wire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…UN, instead of caching a tree that grades every attempt UNGRADEABLE Root cause of the 22% ungradeable rate, glass-boxed on sympy__sympy-11400. The era pin at the top of this block downgrades pytest to the instance's own date, which is right when the era INTERPRETER rung (#2253) found a matching interpreter. When it can't — no Python 3.5 on a modern macOS — the venv falls back to a modern interpreter while pytest stays pinned to the instance's year, and the resulting PAIR can be structurally unable to run. Measured: pytest 2.9.2 (correct for 2016) on Python 3.9.6 dies in `pytest_configure` with INTERNALERROR before collecting anything — reproduced on a two-line trivial test, outside the repo, no conftest, no sympy involved. Every test then "fails", the pristine p2p reads 0/29, the tree grades UNGRADEABLE. 8 of 36 distinct instances on this box are in that state. So: prove the harness executes before handing the env to a citizen. A version pin is a GUESS about compatibility; running it is the evidence. `--version` is not enough — it answers happily for a pytest that dies on any real run. This REFUSES rather than self-heals, and that is measured, not assumed. The obvious repair (reinstall a modern pytest) was tried against this exact tree and does NOT work: pytest 8.4.2 → loads, dies in sympy 1.0's 2016 conftest on the `py.path` hook API removed in pytest 7 pytest 6.2.5 → dies on `py.test.mark.slow`, removed in pytest 4 pytest 2.9.2 → cannot run on Python 3.9 at all The band that both RUNS on 3.9 and LOADS a 2016 conftest is EMPTY. No version choice rescues this class, so an auto-repair would silently trade one void tree for another. What DOES work, verified on this tree — 30/30 passing — is sympy's OWN runner (`sympy.test(...)`) on the same interpreter. That is #383's shape ("django needs its OWN test runner") generalised: the runner is a property of the repo era, not a pytest version to search for. `run_tests` is pytest-only today, so until it grows a runner seam this env genuinely cannot produce a verdict — and it now says so loudly, naming the incompatibility and pointing at the runner gap, instead of caching a broken env for every later run to inherit. [[brittleness-is-the-highest-priority-work-there-is]] — heal what is known-safe, REPORT what needs a human decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… parallel runner (Joel's ruling, + STOP gate) The consequence that makes it law: the learning flywheel consumes ROOM TURNS (L1 lifts tool-traces from captured turns, L2 triggers on turn-completion). A detached agent/solve writing progress/<run>.grade.json produces NO turns — so a citizen can burn 12 acts, write a patch, take a graded verdict, and none of it reaches the curriculum. Maximum effort, zero learning. That, not the pass rate, is why benchmarks have failed. Names what is parallel today (ledger files, scraped probes, a second board projection in fold_run_card, private grade.json), the target shape (import task+oracle only → project into a recipe → the ROOM is the runner → grading is the activity outcome → learning falls out because the work happened as turns), and a one-line acceptance test: can a citizen standing in the room perceive the run's state through the same ViewState pipe the human's screen uses? Adds a CLAUDE.md STOP gate over benchmark.rs / agent/solve.rs / swe_bench.rs so an agent arriving under amnesia must read it before touching run state. Written because that is exactly what happened: this session shipped two correct fixes that HARDEN the parallel path instead of dissolving it, including adding a field to a benchmark probe so external consumers could parse it better — which is the smell the doc now names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…room resolves to what it IS (#6/#274/#329) `activity/spawn` has always published a room→recipe binding to the wall, and its own doc says why: "Without this the room forgets which recipe it is and every client falls back to projecting it as a plain chat room." That was accurate. The binding had NO READER. `RECIPE_WALL_CATEGORY` appeared in exactly one file — the writer — plus a test asserting the string equals "recipe". So the whole recipe layer was live and inert at once: recipes authored as data, a `RecipeExperienceSource` projecting them, four shipped manifests including a benchmark carrying scoreboard/central/feed regions — and `DefaultRoomPurpose` answering "chat" for every room in existence, so none of it ever resolved. A benchmark run's room and a chat room were the same object to every renderer AND to the citizen standing inside one. That is what "benchmarks are a parallel system" looks like at the substrate: not a missing feature, a write with no reader ([[a-correct-check-that-nothing-calls-is-nastier-than-a-missing-one]]). - `experience/binding.rs` — `RoomRecipeBinding` + `project_binding`, the typed body and the one rule for turning a room's wall into its identity. Sibling of `standing.rs`, same shape and the same fail-loud stance: no binding is `Ok(None)` (a bare `airc join` makes a chat room), an UNREADABLE binding is an error, never a silent downgrade to chat. - `ipc/recipe_room_purpose.rs` — `RecipeRoomPurpose`, the `RoomPurposeSource` impl the seam's own doc has been waiting for. Event-invalidated cache, not a per-read fetch: `purpose_for` is sync and sits on the projection's store path, so an owner task folds `wall:changed` and re-reads the authoritative binding (the supersede chain is airc-owned and cannot be reconstructed from a delta — same discipline as the wall projector). Seeds every subscribed room at boot so an activity spawned before this core booted resolves without waiting for someone to re-pin something. - `activity/spawn` now SERIALIZES the shared type instead of a hand-authored `json!`. Both sides agree by construction — which mattered exactly zero while nothing read it, and matters permanently now. - `positron_source::spawn` takes the purpose source by injection; boot passes the live index when a daemon is present, `default_source()` (every room → chat) when headless. Honest edges, all pinned by tests: an unbound room, an unreadable binding, and a failed read all resolve to "chat" — the seam is total — but the two failures say so LOUDLY on the probe stream (`activity.purpose.unreadable_binding`, `activity.purpose.read_failed`). A binding naming a purpose no recipe declares resolves verbatim, and `RecipeExperienceSource` then honestly returns no manifest rather than substituting one. Known follow-up, named rather than hidden: this adds a FOURTH node airc reader (presence/wall/kanban/purpose). Consolidating them onto one attach is real work and is not this commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ndistinguishable from a dead one
First live test of the purpose index produced zero probes, and I could not tell from
the evidence whether that meant "attached, seeded, nothing bound" or "never spawned".
The store on disk settled it, but only because I went looking for a sqlite file — the
component itself said nothing either way. That ambiguity is the defect
([[a-correct-check-that-nothing-calls-is-nastier-than-a-missing-one]]).
- attach logs like its three sibling node readers (presence/wall/kanban all do)
- `activity.purpose.seeded` on every boot with {rooms, bound} — "0 bound of 1" is a
FACT, and a different fact from silence
- `activity.purpose.refreshed` on every wall-change cue, so the invalidation path is
observed rather than inferred (wall changes are rare; the probe costs nothing)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…AND mind (the pattern) `ACTIVITY-ROOM-PATTERNS.md` has said this since it was written: "the same transform serves a human's eyes and a persona's mind, because RAG is a render target, not a separate pipeline" … "the human's UI and the persona's grounding are the same projection rendered two ways — they cannot drift, because there is one definition" … "Never render the two from separate code." The code rendered them from separate code anyway. The human's roster comes from `RosterViewState` on the served Substrate; the citizen's came from `persona::room_roster_source` — a second reader, own fetch, own freshness, own failure modes. Same for the board (`KanbanViewState` vs `room_board_source`) and the wall. Three parallel pairs, and one of them was MEASURED delivering a live peer's name ZERO times into a citizen's prompt while the browser rendered that peer fine. This is the compression, not new architecture: - `RagRenderable` — a tiny per-kind impl: KIND (the SAME const the web subscribes to), block label, expand verb, measured floor, salience-ordered units, room scope. - `ViewStateRagSource<V>` — ONE generic adapter making any such kind a `RagSource`. Budgeting, packing, cursors, token estimation, honest-empty, and the room gate are written once. N kinds cost N small impls and zero new plumbing. Properties that fall out rather than being bolted on: - **Cannot drift** — the adapter reads the SAME Substrate the WS server serves. - **Freshness** — no second fold to lag, so the #346 staleness class (citizen trusts an empty board while the announcement is fresh) can't recur here by construction. - **Degrades** — units pack most-salient-first, so a tight window yields FEWER members, never a chopped one (the property `floor_tokens` exists to protect). - **One room gate** — reuses `room_scope_allows`, the shared predicate, rather than a second copy of the same decision. Outlier-validated per CLAUDE.md's methodical process, both in one file so a bad abstraction fails immediately: - A: `RosterViewState` — people, identity, room-scoped. The measured defect's cure. - B: `BenchViewState` — numbers, no identity, per-row verdicts, node-scoped. B needed ZERO adapter changes, which is the whole test. chat / kanban / wall / serving / nav / foundry are now registrations, not builds. 5/5 tests green, each with a `// what this catches:`. One test's arithmetic was wrong on the first run (budget 12 fit all three ~4-token member lines); the PACKING was correct and the test was fixed to 8 — noted in the test itself, because "make the failing assert pass" is how a real invariant gets quietly weakened. NOT YET WIRED into prompt assembly — the seam exists and is proven; swapping the three bespoke sources over is the next commit, so the swap can be reviewed as a behavior change on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…someone swaps a source onto it The obvious next move on `viewstate_rag` is to rebind `room_roster_source` at supervisor.rs onto the new adapter. Reading the substrate first showed that would be a REGRESSION, so the module now says so at the top where a future reader will actually be standing. The `Substrate` cache is keyed by KIND ALONE — positron's own `revisions.rs` names the `(room_id, kind)` tuple as a future extension. So the node substrate holds ONE room's roster: the focused room's. Swapping today means this adapter's room gate correctly abstains for every persona whose turn is in a different room, and personas are first-class MULTI-room subscribers. Most citizens would go BLIND rather than mis-sighted — trading "sometimes wrong" for "reliably empty" is not a repair. Filed the prerequisite as #408 (per-room substrate key) with the acceptance test: two personas in DIFFERENT rooms each receive THEIR room's roster in one tick. This is the note I would have wanted before shipping the swap, not after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…n read HER room (#408) The node's cache is keyed by KIND ALONE. `scoping.rs` said it in its own words — "Everything else is per-room and stays on the node substrate" — which is the bug in one sentence: those kinds are TREATED as per-room but share ONE store, so the node holds whichever room wrote last (the FOCUSED room) and every other room reads empty. That is what blocks the citizen side of positron. `persona/viewstate_rag.rs` makes any ViewState a RagSource so a citizen and a browser read ONE definition — but a persona is a first-class MULTI-room subscriber, so under one shared slot most citizens would get an EMPTY roster rather than a wrong one. "Reliably blind" is worse than "sometimes wrong", which is why that swap was NOT made first. The fix is not new machinery — it is the SECOND INSTANCE of a pattern already here: `PER_USER_KINDS` + `PerUserSubstrates` already solve "N scopes share a kind namespace" for citizens (nav). This adds the room axis in the same shape: - `PER_ROOM_KINDS = [chat, roster, kanban, wall]` — open by data, like PER_USER_KINDS - `PerRoomSubstrates::for_room(room)` — one substrate per room, created on first use - `CompositeCache` routes THREE scopes: per-user → citizen store, per-room → room store, everything else (bench, serving, system-metrics) → the node store, because those describe the NODE and have no scope to route to. ADDITIVE ON PURPOSE. `CompositeCache::new` is preserved verbatim and still resolves room kinds from the node store, so every existing caller keeps today's behavior; scoping is opt-in via `CompositeCache::scoped`. A migration that silently re-pointed every reader would make "did this change anything?" unanswerable. 8/8 green. The acceptance test is the one that matters: `two_rooms_each_keep_their_own_state_in_the_same_tick` — two rooms, each reads ITS OWN state, neither overwritten. Plus: writer and reader of one room share ONE store (no second fold to go stale — the #346 class), the unscoped constructor is unchanged, and the three-way route neither merges nor leaks across scopes. No positron-core change, no wire change, no tag bump — entirely in-tree. My first estimate of this task understated it and my second overstated it as a cross-language contract change; reading `scoping.rs` settled it as an in-tree application of an existing pattern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… one fold, two sinks (#408) Per-room substrates existed as of the previous commit and were EMPTY: nothing wrote to them, so they were a correct mechanism with no data — the same write-with-no-reader shape as the recipe binding, inverted. `ChatProjection::store` now mirrors every per-room envelope (chat, roster, the Experience manifest) into that room's own store via ONE helper, `store_room_scoped`. One place decides the dual-sink rule, so a future kind cannot be added to one sink and forgotten in the other. ONE FOLD, TWO SINKS — not two folds. The projection computes the view once and the SAME `StateEnvelope` (same revision) lands in both stores. A second FOLD is what goes stale (#346, where a citizen trusted an empty board while the announcement was fresh); a second SINK of one fold cannot drift from itself. Web behavior is untouched: the node substrate still receives everything exactly as before, so the focused-room session reads what it always read. The per-room stores are additive and, until a consumer names its room, unread. `rooms: None` in tests and headless keeps today's path. 19/19 green. The new test is the crux: two rooms speak, B last; the NODE ends on B (unchanged focused-room behavior) while room A's OWN store still holds A's view with A's message. That is precisely the state the single-slot cache used to destroy, and it is what `ViewStateRagSource` needs in order to hand a citizen HER room. Still not wired to a consumer — ipc/mod.rs must construct the registry and pass it in, then the roster source can flip. Kept separate so the wiring is reviewable as its own behavior change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…n the browser renders (#408) The last two wires. The roster a persona grounds on is no longer a second reader of airc — it is `ViewStateRagSource<RosterViewState>` over HER room's own store, which is the SAME `RosterViewState` the web roster renders. - `ipc::global_room_substrates()` — the process-global `PerRoomSubstrates`, same `OnceLock` shape as `global_nav_focus`. The WRITER (the chat projection, in the WS boot block) and the READER (a persona's grounding, bound at spawn in supervisor) are constructed in different places and MUST land on one registry; two registries would be two stores, which is the exact defect being removed. - `positron_source::spawn` takes the registry and threads it to the projection. - `supervisor` binds the ViewState-backed roster instead of `RoomRosterSource`. This is the repair for the measured defect: a live peer's name appeared ZERO times in a citizen's prompt while the browser rendered that peer fine, because the two read different code. Now there is one definition and two render targets — eyes and mind cannot drift, because there is nothing to drift from. `room_roster_source` is left in the tree untouched (still used by the presence emitter and the experience resolver); it is simply no longer the persona's roster. No dead-code scaffolding was added to "preserve a rollback" — `git revert` is the rollback, and a dead fn kept for comfort is clutter. 24/24 green across positron_source + viewstate_rag. NOT yet live-verified — the acceptance test is a real turn's prompt capture containing a peer's name, which is the next step and the only evidence that counts here ([[never-blind]]: a fix I cannot prove reached the running binary is a fix I have not made). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…et reaches the server Two defects, both introduced by 7e0c546 ("`continuum start` execs the installed server"), both in the ONE function `start` and `reboot` share. The change was right for `start` and wrong for `reboot`, and `launch_core` had no way to tell them apart. 1. THE SOCKET WAS NEVER PASSED. The direct-exec path handed the socket to the server over CONTINUUM_CORE_SOCKET and omitted the positional argument. `main.rs` requires argv[1] and exits 1 with its usage text without it — so on any machine with an installed binary, every `start` and every `reboot` died ~2s in, having written Usage: continuum-core-server [--mode=<MODE>] <socket-path> into the start log and nothing else. Measured here tonight: the reboot killed the old core's claim on the swap, the new one never came up, and only the surviving old process kept the system answering. 2. REBOOT STOPPED BUILDING. `reboot` is THE deploy path ("edit → reboot → exercise"). Preferring the installed artifact made it structurally unable to ship an edit: it printed "building fresh binary, then swapping" and exec'd a binary from Jul 13. Every fix made since would have been invisible to the running core, and the only thing standing between that and a false success line was #194's provenance check. The fix makes the source policy an explicit argument instead of an ambient default, because the two callers want opposite things: `start` wants a core RUNNING (installed artifact is correct, and the no-source-tree user 7e0c546 was written for keeps working), `reboot` wants the core built FROM THIS CHECKOUT. `plan_launch` resolves (policy, env override, script?, artifact?) as a pure function — 7 tests, one per branch, each naming the failure it prevents. A reboot on an installed node with no checkout still restarts the artifact, but through a distinct variant that forces the CLI to SAY nothing was rebuilt rather than let a restart pass as a deploy. Server side: `continuum-core-server` was the one component in the tree hand-rolling its own socket resolution (argv or die) while `continuum`, `continuum-mcp` and every library caller go through `endpoint_paths::core_socket_path()`. argv[1] still wins; absent it, the server now agrees with everyone else instead of exiting. That disagreement is what let defect 1 exist at all — the launcher communicated over a channel the listener had never been told to read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…very deploy `continuum start` execs the INSTALLED continuum-core-server (building is reserved for `reboot`), and the resolver checks ~/.continuum/bin BEFORE any cargo target dir. But that copy was written once by install.sh and never refreshed by a deploy — so on a machine that has been deploying for a month, `continuum start` silently boots a month-old core while the freshly-built one sits unused in the cache. Measured on the M5 tonight: installed artifact dated Jul 13, running build 4705, HEAD 4712. And it was not theoretical — a stray auto-start off that stale copy during a reboot is exactly what tripped the #194 deploy-provenance mismatch and cost an hour of misreading a deploy that had actually built fine. This script already publishes the CLI into ~/.local/bin on every deploy, with a comment arguing precisely this ("refreshes each deploy so PATH always points at the current build"). The core-server was simply omitted from that reasoning. Now it isn't. Placed AFTER the #194 freshness guard and before exec, so the installed artifact is only ever replaced by a binary just proven to match source — never a stale or half-built one. Atomic temp+mv so a concurrent `continuum start` cannot exec a half-written file. Non-fatal, and it says out loud when it cannot publish, because the consequence the operator needs to hear is "`continuum start` may boot an OLDER core than this one". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…or the way every other RagSource does CI red on PR #2282, my defect. `cargo test -p continuum-core --lib` failed one test out of 7,042: cognition::context_budget::tests::no_new_hardcoded_context_or_prompt_size_constant_anywhere_in_the_crate The guard (context_budget.rs:356-365) matches any `const` whose name contains WINDOW|CONTEXT|CTX|TOKEN|PROMPT|CHARS, of an integer type, assigned a bare decimal literal. `const FLOOR_TOKENS: u32 = 10` (roster) and `= 18` (bench) matched on TOKEN. The number was never the problem — it is a per-UNIT content floor ("one roster line costs ~10 tokens"), not a context bound, and it does not scale with the served window. The problem is that I expressed it in a SHAPE nobody else uses. Every other source states the same fact as a function: room_board_source.rs:293 fn floor_tokens(&self) -> u32 { 32 } room_roster_source.rs:286 fn floor_tokens(&self) -> u32 { 0 } room_doctrine_source.rs:151 fn floor_tokens(&self) -> u32 { 0 } rag_budget.rs:475 fn floor_tokens(&self) -> u32; // the contract So `RagRenderable` grew a SECOND spelling of one contract — an associated const beside the trait method it feeds. That is the duplication the compression principle forbids, and it landed in the very file meant to be the template every future ViewState source gets copied from. The guard fired on the new shape, which is exactly its job. Fix: `const FLOOR_TOKENS: u32` becomes `fn floor_tokens() -> u32`, matching the established idiom. One contract, one spelling; the guard passes as a CONSEQUENCE of saying it the normal way rather than as the goal. Deliberately NOT done: no `// context-budget-exempt:` line (the escape hatch exists, but an exemption would preserve the second spelling — the actual defect), and no weakening of the guard. Guard test: 1 passed. viewstate_rag tests: 5 passed. Clean `cargo check --lib`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ranch refused it (#410) Measured today: every `airc msg` a human sends reaches EVERY citizen's subscribe stream (12/12 raw_events, sender 7711fe60) and is dropped as `no_continuum_body_hint`. Cause is in `realtime_wire::envelope_from_event` (:58-68) — it returns Ok(None) unless HEADER_FORGE_BODY_HINT == CONTINUUM_BODY_HINT, a stamp only continuum's OWN clients apply. The message is refused before its content is ever read, so a human talking to the room over the CLI is structurally unheard while the browser renders it fine (`airc.chat.projected` fires every time). The fix is one more arm in `room_turn_from_event`. It needs the CLI's actual body shape, and the probe could not supply it: `reason` names the BRANCH that refused the event, never the SHAPE that was refused. Attempts to recover the shape out-of-band failed — `airc events list --kind message` returns 0 in BOTH the project and machine-account scopes while returning 60 system-kind events, so the persisted view disagrees with what delivery demonstrably did. So instrument rather than guess. A decoder arm written against a GUESSED body is exactly how presence and control frames become fabricated perception — the hazard the existing named-skip contract exists to prevent. Next session reads one real body and writes the arm against a fact. Adds `event_kind` (the TranscriptKind, which IS the receive-side discriminator — note FrameKind lives on Frame and does NOT survive to TranscriptEvent) and a 160-char `body_preview` to the ALREADY-firing filtered_non_turn line. No new probe class, no new event, and the stream-chunk skip above stays deliberately unprobed — this cannot reintroduce the flood that skip removes. Behaviour unchanged: diagnostics only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
Canary had evolved past this branch on every conflicting file (46 files). Resolution rule, applied uniformly: canary wins conflicts, this branch's non-conflicting changes retained. experience/binding.rs was an ADD/ADD — both branches created it independently. Canary's is a strict superset (typed `parent: Option<RoomId>` instead of `String` per [[uuids-are-not-strings]], plus #433 recipe params). Verified zero symbols exist only on this side before taking it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ed at (#440/#441) MEASURED 2026-08-20, Qwen3.8-27B live on the M5. MTP spec-decode is working: 80.3/82.3/84.3% draft acceptance across 3 samples, lane running `--spec-type draft-mtp --spec-draft-n-max 4 --spec-draft-p-min 0.7`. That half of #440 is proven end-to-end. What is NOT comparable: the catalog's 17.2 tok/s was taken at a 19,712 window; the live lane serves 89,280 — 4.5x the KV to walk per decoded token on UMA. Samples came in at 2.7/6.2/7.8 tok/s. The #441 collapse alarm is correctly calibrated (0.25 floor: it stays silent at 0.36/0.45 and FIRES at 0.16) but its warning text names CPU fallback, pager thrash and GPU contention as the suspects — so a reader would hunt a defect that is not there. Decode rate is a function of resident KV. A rate without its window is not a comparable quantity. ThroughputBaseline already refuses to "present an unsourced baseline as fact"; an unwindowed one is the same class, so the window joins source as provenance. - `measured_at_window: Option<u32>` — None means genuinely not recorded, an honest absence rather than a 0 sentinel ([[unknown-is-not-a-quantity]]). - `comparable_at(live_window)` — false means "this baseline cannot say", NOT "healthy". A caller that alarms anyway must state the window gap. - Existing seed rows get None: their windows were never recorded and I will not invent them. 8/8 module tests green; cargo check exit 0 (verified by exit code — a grep-filtered check hid failures from me earlier today). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…merge My merge 9e2389f shipped union-merge damage (duplicated lines, orphaned closers) in 33 files and I reported it as compiling clean. It did not: I was validating with `cargo check | grep ": error"` and reading empty output as success, which silently hid the failures. Reading $? gave 101. Every one of those files is superseded by canary anyway — canary won all 46 conflicts in that merge, verified file by file — so restoring them loses nothing and repairs the tree. VERIFIED THIS TIME BY EXIT CODE, not by grep: cargo check -p continuum-core --lib --features metal,accelerate -> 0 cargo test ... throughput_expectation -> 8 passed, 0 failed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
THE CI FAILURE. `cargo test -p continuum-core --lib` regenerates ts-rs bindings and compares against what is committed; these four had union-merge damage and could not match. The damage is the same class I spent the night repairing in the Rust — I just never looked at the generated files, because "generated" read to me as "not mine to inspect". RoomRecipeBinding.ts closes the type declaration MID DOC-COMMENT (`parent?: string, };` with the rest of the comment dangling after it); the three memory params files import the same symbol TWICE from two different paths. Why the compile check stayed green while this was broken: nothing compiles `.ts`. windows-msvc passed `lib + tests` the whole time. Why the guard built for exactly this said nothing: `ts-rs binding drift detector` is `needs: cargo-test-continuum-core`, so when that job failed the detector reported `skipping`. The check designed to catch binding drift was gated behind the job the drift broke — worth its own card. Restored from canary. The branch is now exactly canary + the throughput change, nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
This was referenced Aug 20, 2026
joelteply
added a commit
that referenced
this pull request
Aug 21, 2026
…440/#441) (#2341) * docs(catalog): the 27B's contended rate, recorded as contended (#440/#441) MEASURED 2026-08-20, 6 samples of 150 tok on the live M5 lane: 4.74 / 5.25 / 6.55 / 6.57 / 6.66 / 6.67 tok/s, median 6.56, at a 22,528 PER-SLOT window with `busy_slots = 2` of 4 on EVERY sample. NOT written into `tokens_per_second`, deliberately. That field is what #441's collapse alarm reads as the SINGLE-STREAM expectation, and 6.56 is a contended rate — `ThroughputBaseline`'s own doc says "single sequence". Putting it there would drop the alarm's floor from ~4.3 to ~1.6 tok/s and let a genuine collapse pass silently. The pinned 17.2 stays. Both numbers are true; they measure different machines. THE GAP IS THE POINT: the sentinel cannot currently tell "contended" from "degraded" because nothing records concurrency at measurement time. That is the sibling of the window axis landed in #2339, and it is #441's remaining half. MTP held 83.9-84.8% draft acceptance across all six, and 80.3-84.3% across three earlier samples on a different prompt. Acceptance is FLAT while t/s moves 1.4x, so the draft head is not the variance source. Also corrects my own earlier reading: I took `-c 89280` off the lane argv and called it the live window. It is the TOTAL across 4 slots; per-slot is 22,528 against the catalog's 19,712 — 1.14x, not the 4.5x I claimed. The window was never the explanation. cargo check exit 0; model_registry tests exit 0 (both read by exit code). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo * feat(serving): the collapse alarm reports concurrency, so a reader can attribute it (#441) The alarm compared a live decode rate against a SINGLE-STREAM expectation and named three suspects — CPU fallback, pager thrash, GPU contention — with no evidence for any of them. Measured 2026-08-20 on the 27B: 6.56 t/s against a 17.2 pinned expectation, ratio 0.38, with TWO model calls in flight. That is real contention, and a reader handed only the ratio would have gone hunting a CPU fallback that does not exist. Now the warning carries `inflight_model_calls` and says what it means: >1 in flight and the expectation is not like-for-like; at 1 in flight the original suspects stand. ANNOTATE, NOT GATE — deliberately. This alarm's own contract lists "contended GPU" as something it EXISTS to catch, so suppressing on concurrency would defeat its purpose and weaken a guard. The firing decision is untouched; only the evidence attached to it changed. REUSES the existing gauge rather than counting again: `resource_admission::inflight_model_calls()` is already public, already maintained by an RAII guard, and its doc says it "reflects exactly the model-call window (lane-queue + prefill + decode)" — precisely the axis needed. The concurrency sibling of the window axis landed in #2339. cargo check 0; throughput + resource_admission tests 0 (read by exit code). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
joelteply
added a commit
that referenced
this pull request
Aug 21, 2026
… through it (#124/#441) (#2342) * revert(serving): remove comparable_at + its invented 2.0 factor — speculative interface, zero callers (#441) Landed and removed the same day. Three defects in one small addition: 1. MAGIC CONSTANT. `WINDOW_COMPARABILITY_FACTOR: f64 = 2.0` was invented. Nothing derives it. The doc rationalised 2x after the fact. 2. ZERO PRODUCTION CALLERS. `comparable_at()` was never called anywhere in the tree. A predicate built for consumers that do not exist. 3. A LYING COMMENT, which is the worst of the three. Each of the three seed rows carried "consumers get `comparable_at() == false` rather than a silent assumption" — asserting a reader that was never written. Same class as #151/#357, authored while fixing that class. ARCHITECTURALLY it was also the wrong shape (Joel): whether two operating points are comparable is a CONSUMER's judgement, made against the lease/handle it already holds. It is not a rule a data struct asserts for every caller in the tree. We have consumer/handle interfaces for this; I bolted on a bespoke free predicate instead of using them. `measured_at_window` STAYS. It is provenance — it says what the number describes, and its absence caused a real live mis-attribution. A field that records a fact is not the same as a predicate that judges one. cargo check 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo * fix(cognition): the de-hardcode guard was blind to floats — and something was already through it (#124) THE HOLE. The guard fires on `const NAME: <int> = <literal>` when NAME says WINDOW/CONTEXT/CTX/TOKEN/PROMPT/CHARS. Its type list stopped at the integers, so ANY window-relative constant typed `f32`/`f64` walked straight past it. Found the hard way: `WINDOW_COMPARABILITY_FACTOR: f64 = 2.0` (mine, #2339) matched the name rule, WAS a bare literal, and sailed through on the type. A guard against invented numbers that a float annotation defeats teaches exactly one lesson — type your magic number as a float. A ratio over the window is as much a fresh guess as a count of its tokens. Now caught the same way: f32/f64 added, and the literal detector accepts a decimal point (one only, digit-led, so `1.5` matches and `A.b`/`1..2` do not). WHAT IT CAUGHT ON THE FIRST RUN — not mine, and pre-existing: recall_faculty.rs:70 — const RECALL_WINDOW_FRACTION: f32 = 0.10 10% of every citizen's served window, spent on recall, invented and never derived. Exactly the shape the guard exists to stop, sitting inside the guard's blind spot. That is the positive control: the hole was real and occupied. THE FIX is the one the guard's own failure message prescribes — express it as a fraction on ContextBudget, where it SCALES with the served window instead of being a 4k-shaped slice a 1M-context model inherits. `ContextBudget:: recall_tokens()` over `RECALL_DENOM`; recall_faculty derives from it via the existing `from_window` constructor and no longer owns a float. MIN_RECALL_TOKENS stays as-is — it carries a real `context-budget-exempt:` receipt saying it only ever RAISES a budget, never caps one. cargo check --tests 0; context_budget + recall suites 0 (read by exit code). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --------- Co-authored-by: Claude Opus 5 (1M context) <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.
MTP spec-decode is working live on the M5: 80.3/82.3/84.3% draft acceptance across 3 samples, lane running
--spec-type draft-mtp --spec-draft-n-max 4 --spec-draft-p-min 0.7at an 89,280 window. #440's three pieces (catalog row, draft resolution, spawn flags) are proven end-to-end.The finding: the catalog's 17.2 tok/s was measured at a 19,712 window; live is 89,280 — 4.5x the KV per decoded token on UMA. Samples: 2.7/6.2/7.8 tok/s. #441's collapse alarm is correctly calibrated (silent at 0.36/0.45, fires at 0.16) but its text blames CPU fallback / pager thrash / GPU contention, sending a reader after a defect that isn't there.
So
ThroughputBaselinenow carriesmeasured_at_window: Option<u32>(None = honestly not recorded, not a 0 sentinel) pluscomparable_at(), whosefalsemeans this baseline cannot say — not healthy.Also repairs my bad #2282 merge: 33 files restored to canary's version. Canary won all 46 conflicts there, so nothing is lost.
Verified by exit code, not a grep filter (which hid failures from me earlier):
cargo check-> 0, throughput tests 8/8.🤖 Generated with Claude Code
https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo