fix: five silently-unwired capabilities — CI pointed at a deleted dir, deps that never re-installed, an engine nobody asked its version, a memory bridge that couldn't find airc, and a plugin frozen 2 weeks behind - #2213
Open
joelteply wants to merge 8 commits into
Conversation
…hat should have was pointed at a deleted directory Three defects that compose into one: the TypeScript client tests were unguarded, the guard that should have covered them was dead, and on Windows the suite could not be run at all. 1. THE SUITE WAS RUN BY NO WORKFLOW. `npm run test:clients` was already in package.json and already passing. Nothing invoked it. So renderBench.spec.ts and six sibling spec files across apps/web and packages/ existed, were green on their authors' machines, and gated nothing — a correct check nothing calls, indistinguishable from having no check until someone greps for the caller. 2. ci.yml HAD BEEN RED FOR TWO MONTHS. Last success 2026-06-07; failing continuously from 2026-06-09 with `Cannot find module 'dotenv'`. Root cause was not dotenv: the job ran in `working-directory: src` against `src/package-lock.json`, calling `npm run build:ts` and `npm run test:crud`. `src/` was the Node monolith, retired when the substrate became a headless Rust core (#1840). Every path and script it named is gone; the error was describing the absence of its whole world. A check that ALWAYS fails is worse than no check — it cannot distinguish a broken PR from a healthy one, so the only lesson available is to stop reading CI. That is what happened: work routed around it through the Rust and drift-guard workflows for two months. Replaced rather than patched, and it now watches `canary` as well as `main`, because a gate that only sees the stable line learns about breakage after the merge. 3. THE SUITE COULD NOT RUN ON WINDOWS AT ALL. `@continuum/web` and `@continuum/chat-view` declared `"test": "TZ=UTC vitest run"`. npm runs scripts through cmd.exe on Windows, which has no POSIX env prefix, so both died with `'TZ' is not recognized as an internal or external command`. The other three workspaces use a plain `vitest run` and were fine — the split is exactly the prefix. Fixed by REMOVING the portability assumption rather than packaging a tool to satisfy it: apps/web already pinned `test: { env: { TZ: 'UTC' } }` in its vite config, making the prefix redundant; chat-view had no config at all, so it gets one whose only job is that pin. No cross-env dependency added. Worth stating: this one is only findable by RUNNING the suite on Windows. Linux CI and macOS both pass. Adding the CI gate alone would have gone green and left every Windows contributor unable to run the tests locally — the gate would have hidden this rather than caught it. Verified on Windows after the fix: 25 test files, 153 tests, exit 0. Before it, the two affected workspaces produced no test run whatsoever. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
… client scripts only
`install.sh` runs `npm install` ONCE. Nothing re-ran it when the manifest
changed. So a contributor who installed in June and pulled in August had a
node_modules that silently did not match its own package.json, and the first
symptom was every client spec failing at COLLECTION with:
Failed to load url @continuum/chat-view. Does the file exist?
which points a newcomer at missing SOURCE rather than at their missing deps.
Measured on a real checkout: tree from Jun 17, manifest from Aug 5, `lit` and
every @continuum/* workspace package absent, seven spec files dead.
WHERE IT HANGS, AND WHY NOT THE START PATH. `start-server.sh` is headless Rust
by doctrine — "No Node, no TS, no widgets. The Node orchestrator stays out of
the loop." A dependency guard wired there would drag npm into the one runtime
path that exists to avoid it. So this hangs off `pre*` hooks on the CLIENT
scripts only: dev:web, dev:desktop, build/lint/typecheck/test:clients. Anyone
who only ever runs the core pays nothing and never sees it execute.
mtime, not a checksum: npm writes node_modules/.package-lock.json when it
materialises the tree, so "tree older than manifest" is exactly the question
worth asking and needs no parsing. A checksum would be more precise about
CONTENT and no more precise about what actually breaks people.
LOUD, never silent. A guard that repairs things without saying so teaches the
operator that installs are magic and hides a real signal — a lockfile moving
under them — that is sometimes worth knowing. Skipped under CI, where `npm ci`
is authoritative and already ran; CONTINUUM_SKIP_DEP_CHECK opts out for anyone
hand-managing a tree.
Verified both directions: silent on a fresh tree, fires with the reason named on
a stale one, no-ops under CI=1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
…its own name Four names were in play and two were installed: uu ON PATH core/continuum-core/src/bin/continuum.rs continuum ON PATH same build, same size, same mtime — one binary, two names ctm absent declared as [[bin]] in apps/cli/Cargo.toml, never installed jtag absent used 40 times in CLAUDE.md, installed by nothing (Node era) So the declared name, the documented name, and the installed names were three different answers and nothing reconciled them. Joel reached for `uu` and was right; I nearly "corrected" him from the stale Cargo.toml, which would have been ratifying a declaration over the live system. WHAT CHANGED `program_name()` derives from argv[0]. The usage text was the literal string "usage: continuum ...", so `uu --help` printed examples for a command the reader had not typed — the front door did not know its own name. One binary can now ship under any number of names and each tells the truth; a future alias is correct the moment it exists, with nothing to remember to update. `version` is handled LOCALLY and never dispatched. `uu version` used to fall through to the substrate and return `Unknown command: 'version'` — the CLI could ask the core what IT was and could not say what ITSELF was, and with no core running it answered nothing at all. That is the gap that lets someone debug a fixed bug with an unfixed binary in their hand, which is the whole point of the version ruling. CLAUDE.md: 38 `./jtag` invocations rewritten to `uu`. Deliberately NOT rewritten: `.continuum/jtag/logs/...` paths (3, real directories), the [[jtag-probes-are-rtos-debugger]] memory slug, and the legacy JTAGClient snippets — none of those are commands, and a blind replace would have broken log paths. The mistake entry itself was inverted and is now the correction: it told every new session "Always work from `src`" and "Commands: `./jtag` NOT `./continuum`". `src/` is the retired Node monolith, `jtag` is installed by nothing, and `continuum` — the one it warned against — works. That is a documentation lie with a live cost: it is the first thing a fresh agent reads and the first ten minutes it wastes. Env vars corrected mid-edit: I wrote CONTINUUM_BUILD_SHA/_BRANCH from memory; build.rs emits CONTINUUM_BUILD_GIT_SHA and no branch at all. `option_env!` would have silently printed "unknown" rather than failing, so that would have shipped as a quiet lie about the build. cargo check -p continuum-core --bin continuum: 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
…nfo on the snapshot A per-slot wedge was attributed to "the fork bump in build 4577". 4577 is the Rust CORE's build number; the engine has its own, unrelated one, and the fork engine had not been rebuilt at all. Settling it took comparing binary mtimes on two machines. I told M5 the engine had no version surface. That was wrong: llama_build_info() has existed all along — `--version`, the startup log, `/props.build_info`, and `system_fingerprint` on every completion. The gap was entirely on OUR side. The daemon already GETs `/props` for the served window and the modalities verdict, and threw `build_info` away. So this is silently-unwired-capability with the polarity reversed: not something we built and failed to wire, but something upstream hands us free that we drop on the floor. Same cost — a fact available for the asking gets re-derived by archaeology, and the derivation was wrong. - LlamaServerControl::engine_build() reads /props.build_info, default Ok(None) so fakes and remote controls stay honest by construction - rides on ServingSnapshot, stamped at reconcile, probed on the reconcile line - surfaces as engineBuild on ai/inference/status The commit sha is the load-bearing half: build numbers are ancestor counts, so our fork and upstream can both say b6789 and mean different code. Three assertions pin it: the identity must reach the snapshot; an engine that cannot say what it is reads as unknown rather than a guess; and a not-live snapshot never names an engine even when one answered the probe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
… CUDA's business Two defects, both found by a plain `cargo check` dying with "is `cmake` not installed?" on a box where cmake was installed a directory away. 1. The cmake + ninja pins lived INSIDE windows-build-env.sh's CUDA block, gated on `nvcc present AND cl.exe absent`. Neither is a CUDA concern. A CPU-only Windows box (no nvcc) skipped the block and got no cmake pin at all; a shell where cl.exe already resolved skipped it for the same reason. Hoisted into two self-guarded blocks that run on their own applicability. The PATH half is now the manifest's job — [module.runtime_path] on cmake and ninja, consumed by the generic loop that was already there. That is where the windows-vs-unix split belongs, because it IS a packaging fact: brew/apt put these on PATH, the Windows archives do not. Linux/macOS projections carry nothing, as they should. 2. ninja was fetched by a HARDCODED url inside the llama-server PowerShell module — no manifest entry, no pinned sha256, invisible to the manifest-gen drift gate. One tool provisioned by different rules than every other tool is exactly the drift the manifest exists to prevent, and an unverified download is a supply-chain hole however convenient the url is. Now a real Mod-Ninja with the same guard shape, sha256 verification and source-of-truth as Mod-CMake, and it runs in install.ps1 beside Mod-CMake instead of only on boxes that had already built llama-server with CUDA. ninja is declared platforms = ["windows"] deliberately. The defect it fixes is Windows-only (cmake auto-picks the newest VS; "Visual Studio 18 2026" is a generator cmake 3.30.x cannot name). On unix the default generator is never broken, so listing macos/linux would make those contributors install a package to buy nothing. Adapting to the platform means stating the asymmetry, not smearing one platform's workaround across all three. Verified: manifest-gen --check OK (4 files in sync), ninja absent from the linux and macos projections, CMAKE and CMAKE_GENERATOR=Ninja both resolve from a fresh shell with no CUDA involved, Mod-Ninja/Mod-CMake idempotent-skip, PowerShell parse + bash -n clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
…own" for "program not found" Engram recall has been silently off on BigMama. Every session start printed "MEMORY BRIDGE DOWN — could not resolve this agent's persona id (airc status gave nothing)" while airc had 21h uptime and 1305/1305 acked. The probe never observed the daemon. Hooks do not inherit the operator's interactive shell — airc installs to ~/.local/bin, routinely absent from a hook's PATH — so bare `airc` was not a program it could find, and 2>/dev/null swallowed the "command not found" that would have said so. The same file already had resolve_continuum() doing this correctly. Two answers to "find a binary" in one file, one of them robust, is the drift; now there is one shape (resolve_airc, honoring $AIRC_BIN, with .exe variants for Windows). share.sh's two bare `airc` calls go through it too. And the receipts stop guessing. "No airc binary" and "airc ran and reported nothing" are different types, not two values of one type — conflating them is what wrote "airc status down" about a healthy daemon, twice, and sent an afternoon of diagnosis at the wrong layer. persona_failure_reason() re-derives the measured cause for both the receipt and the notice the AGENT reads. It re-derives rather than setting a variable inside resolve_agent_persona because callers invoke that as $(...) — a subshell, where any assignment dies. The first version of this fix did exactly that and the receipt came out blank. The negative test caught it, which is why it exists. Verified both directions: with ~/.local/bin stripped from PATH (the environment that failed for two sessions) the id now resolves; with no airc reachable at all the failure names the real cause instead of blaming the daemon. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
…indistinguishable from "stale"
The bridge's founding rule is that "installed" must never be indistinguishable
from "working". This is that rule one level up, and it is why yesterday's fixes
would have changed nothing on the machine that needed them.
There are two install paths with very different freshness semantics:
* `claude --plugin-dir tools/plugins/memory-bridge` runs LIVE from the repo —
`git pull` IS the update.
* a marketplace install COPIES the plugin to
~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/ and pins it to a
git sha. Nothing re-syncs it. `git pull` changes nothing.
Nothing told you which one you were on. Measured on BigMama: the running copy
was pinned to 60fa0db (2026-07-25), two weeks stale. Its lib.sh had no
persona-id cache, and it contained NO session-capture.sh at all — so automatic
per-turn capture, the entire "volitional memory isn't memory" point, had never
run once on that machine. The README said the bridge was live the whole time.
So session-recall now compares its own running location against
tools/plugins/memory-bridge/scripts in the current checkout, and on drift emits
⚠️ MEMORY BRIDGE STALE plus a `stale` receipt. A missing file counts as drift —
that is exactly how session-capture.sh went absent unnoticed.
It runs on the SUCCESS path too, because staleness is orthogonal to whether
recall worked: a frozen copy can recall perfectly and still be missing every fix
since it was installed.
Only a cached copy can be stale, so running from the repo stays silent; and if
the cwd is not the continuum checkout there is nothing to compare against, so it
says nothing rather than guessing.
README: the "Status: Live" line was a claim a README cannot make — it describes
the repo, while liveness depends on your install. Replaced with the two paths
and how to tell which you are on.
Verified against the real frozen copy on this box (4 of 4 scripts differ →
notice fires) and against a live repo run (silent).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
…l reason fixes never shipped
Plugin updates are VERSION-based, not content-based. `claude plugin update`
compares the DECLARED version against the installed one and never looks at the
files. memory-bridge had sat at 0.1.0 since 2026-07-25, so every installed copy
answered:
✔ memory-bridge is already at the latest version (0.1.0).
...no matter what changed. That single static string is why two weeks of work
never reached the machine that needed it: the copy running on BigMama had no
persona-id cache and no session-capture.sh AT ALL, so automatic per-turn memory
capture — the whole "volitional memory isn't memory" point — had never run once,
while the repo held working code and the README said the bridge was live.
Bumping 0.1.0 -> 0.2.0 propagated all of it in one command, verified here:
marketplace update -> `Plugin "memory-bridge" updated from 0.1.0 to 0.2.0`, the
new copy carries session-capture.sh + resolve_airc + the staleness detector, and
now differs from this tree in 0 of 4 scripts.
So the guard. check-plugin-version.sh fails when files under a plugin dir change
without that plugin's plugin.json version changing, and it compares the version
BEFORE and AFTER rather than trusting that the manifest was touched — editing a
description is not a release. A brand-new plugin has nothing to bump from and
passes. Verified both directions: passes on this commit's real bump, fails on a
simulated missed one.
Wired into CI, deliberately NOT into .githooks/pre-commit: that hook invokes
tests/adversarial-protocol.test.cjs and tests/command-processing.test.cjs, both
deleted with the Node monolith, and core.hooksPath does not point at it — so no
pre-commit hook runs at all right now. Adding a gate there would have looked
enforced while running never, which is the same defect one level up. Flagged
rather than silently repaired; that hook needs its own decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
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.
Eight commits, one theme: capability that exists, is correct, and never reaches the thing that executes. Each was invisible because the repo looked right — because it was.
1. The clients suite existed and nothing ran it
The workflow was
working-directory: src— the deleted Node monolith — callingbuild:ts/test:crud. Rewritten tonpm ci+typecheck:clients+test:clientson main AND canary.2. The workspace repairs itself when the manifest moves
install.shrannpm installonce, at install time. Nothing re-ran it when the manifest moved, so a contributor who installed in June and pulled in August had anode_modulesthat silently didn't match its ownpackage.json— and the first symptom pointed at missing source. Hooked topre*on the client scripts only;start-server.shstays headless-Rust by doctrine.3. One front door:
uuis the commandThe binary derives its own name from
argv[0], souu --helpsaysuuand any future alias is correct the moment it exists. Reports build number + sha + built-at.4. The daemon asks the engine what it IS
A wedge got misattributed to "the fork bump in build 4577" — a Rust core build number read as the engine's. Settling it meant comparing binary mtimes on two machines. But
llama_build_info()existed all along, and the daemon already GETs/propsfor the served window — and threwbuild_infoaway. Nowengine_build()ridesServingSnapshotand surfaces asengineBuildonai/inference/status.5. ninja becomes a manifest module; cmake/ninja stop being CUDA's business
Both pins lived inside a
nvcc && !cl.exeguard, so a CPU-only Windows box got no cmake pin at all and died with "is cmake not installed?" while cmake sat a directory away. PATH knowledge moved to[module.runtime_path]where the windows-vs-unix split belongs as data. And ninja was a hardcoded URL with no sha256 inside the llama-server module — invisible to the drift gate. Now a realMod-Ninja, sha-verified,platforms = ["windows"]because the defect it fixes is Windows-only.6–8. The memory bridge, and why its fixes wouldn't have shipped
Engram recall was silently off: hooks don't inherit the operator's shell,
airclives in~/.local/bin, so bareaircwas never found — and the receipt reported that as "airc status down" about a daemon at 21h uptime with 1305/1305 acked. Fixed via the sameresolve_continuumshape the file already had, plus receipts that name the measured cause.Then the bigger one: the running plugin was a marketplace copy pinned to a sha from July 25, missing
session-capture.shentirely — automatic per-turn capture had never run once.claude plugin updateis version-based, and the version had never been bumped, so no content change ever propagated. Bumping 0.1.0 → 0.2.0 shipped two weeks of fixes in one command (verified). Addedcheck-plugin-version.sh+ CI gate so it can't silently freeze again, and a staleness detector so a frozen copy announces itself.Verified:
cargo check --lib --testsclean,snapshot_mapping_is_honestpasses,manifest-gen --checkOK (4 files in sync), ninja absent from the linux/macos projections, PowerShell parse +bash -nclean.Deliberately not done: the gate is NOT wired into
.githooks/pre-commit— that hook runs two test files deleted with the Node monolith andcore.hooksPathdoesn't point at it, so no pre-commit hook runs at all. Wiring a gate there would look enforced while running never. Flagged, not silently patched.🤖 Generated with Claude Code
https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc