Review follow-up: `const closed` beside the boolean `this.closed` reads as the
flag, so name the promise `closing` at every site; the test-side comment
repeated the production mechanism verbatim, so each test keeps only the
ordering assumption its fake encodes.
The EPERM path in the Windows coverage failure also matches the HMR
exact-config watcher: it watches the temp harness home with chokidar's
default 2 s write-settle window, and that spec constructs it on the very
directory its afterEach deletes. Apply the same containment to that
watcher and to the skill-filesystem root watcher, the remaining owners
that enable awaitWriteFinish.
chokidar's close() drops every listener while a scheduled write-settle poll
can still run. That straggler stats a document the home teardown is
deleting, which Windows reports as EPERM, and an 'error' emission with no
listener is rethrown as an uncaught exception: the whole coverage
partition fails while every test file passes.
Re-attach the provider's error listener across the close so the straggler
stays inside the provider, and make it a no-op after disposal.
pi-ai 1.0.1 and 1.0.2 bring model catalog updates and adapter fixes:
Anthropic native tool changes define later tools inline, Bedrock binds
replayed thinking blocks, OpenAI-compatible requests can pick sampling
parameters by thinking level, and "model is at capacity" is retried.
The runtime dependencies change only in @anthropic-ai/sdk (0.124.0 to
0.129.0) and @earendil-works/pi-telemetry (^1.0.2).
1.0.2 still re-parses the whole accumulated tool-argument JSON on every
stream delta (earendil-works/pi#9265), so the patch is regenerated
against the 1.0.2 sources: it removes the same six per-delta parses as
the 1.0.0 patch, and only the bedrock-converse-stream.js hunk moves, by a
line.
pi-ai 1.0.0 adds Sign in with ChatGPT to the `openai` provider, and that
login refuses to start unless the app supplies a stable installation UUID
through LoginOptions.getDeviceId. The harness keeps no installation ID
meant for a third-party account sign-in, so the route offers its API-key
login only; a Loader-composed test pins the offered methods.
catalogProvider(), catalogModels(), and catalogProviderIds() now read one
index that leaves out providers without a chat model, so a hand-declared
route under such a key inherits nothing from pi-ai. The browser-worker
stub gains the builtinModels refusal llm-pi-ai imports and drops the
getBuiltinProviders reader nothing imports. The tool-argument streaming
spec also covers the patched Anthropic Messages path.
The chat-model filter left a provider out whenever pi-ai listed no chat
model for it. The browser-worker preview replaces pi-ai with a stub that
lists no models at all, so every provider was dropped and the empty
directory registration failed the llm-pi-ai mount. Only a provider whose
catalog lists models but no chat model is left out now; the stub gains
the `getAllBuiltinModels` reader the filter calls.
pi-ai 1.0.0 adds classifier and image models to the built-in catalog and
ships `typesafe` with classifier models only. The adapter dispatches chat
requests only, so a catalog provider without a chat model gets no entry in
the configurable-provider directory and no sign-in flow.
Move the pi-ai provider adapter from 0.87.1 to 1.0.0 and re-create the
local tool-argument patch for the new dist files. pi-ai 1.0.0 still
re-parses the whole accumulated argument JSON on every stream delta in
the Anthropic, Bedrock, Mistral, OpenAI Completions, OpenAI Responses,
and pi-messages adapters (upstream earendil-works/pi#9265), so the patch
removes the same six per-delta parses; arguments are still parsed once at
toolcall_end.
The release-age exemption moves to the exact 1.0.0 pi-ai and
pi-telemetry versions, and the generated third-party notices and README
links name the new patch file.
The aliases leave tsconfig.base.json, which doc-typecheck maps to built
declarations, and live in the Vitest projects' resolve.alias; the examples
subpath maps to itself there because example plugin entries ship no
declarations. The README's wrapper fence declares its register import so
the fence compiles on its own.
The coverage partitions list *.spec.ts files only, so the published
*.test.ts files reached no partition and the test kit's mount paths went
uncovered there. examples.spec.ts imports them by computed path; the kit
resolves the mod under test from the outermost describe when the file runs
through a spec.
The band's bordered boxes and buttons draw the theme's 0.5px hairline, as
the elevation gate requires; KNOWN_EVENTS is built from ENGINE_EVENTS
instead of repeating the list.
- Disposal cancels detached runs before the engine waits for timer callbacks,
so a callback parked in a $ call ends instead of blocking unload.
- A forgotten band wakes and ends its watch streams; a refresh asked for
during a pass joins one more pass that settles after it; the Web band drops
an ended stream so the next subscriber opens a fresh one.
- $ has Claude Code's namespaces: an unserved member rejects with
'no implementation for <namespace>.<member>' instead of a TypeError.
- A .catch handler's next makes the rewrite check; a tool rename is refused
like an argument rewrite; a null op answer is reported as no answer;
defineMod returns its disposer from apply; the live turn record is copied
before the chain freezes it; prompt.submit is raised only for batches with
a human message and names the submitting mod as origin.
- $.http.fetch bounds the body while streaming; bandColumns/bandRows are
Config; the client package drops its stale gateway edges; the source
overlay lives in the package and anchors its entries to the file, and the
dsh installation no longer depends on the experimental packages.
Hook results spread the typed result from beneath, element props accept an
explicit undefined as a JavaScript mod passes it, the band's registry
payload is narrowed by assertion, and the example wrappers join the Host
typecheck project.
docs/subsystems/claude-code-mods.md lists every way a mod behaves
differently through the bridge than under Claude Code 2.1.287 — packaging,
events, the chain, $ members, drawing, testing, and the example mods,
including the four official mods assessed by reading. The bridge README
describes defineMod, the band, and the test kit; the band package gets its
README; the group index, release families, config and client catalogs,
module and seam graphs, and the Cordis API catalog are regenerated.
A Client package mounts the bridge's Remote contribution, watches each
session's band through watchBand, draws the serialized tree in the input
dock with Claude Code's Box/Text/Button vocabulary on this theme's tokens,
and sends button clicks back through pressBand with the generation they
were drawn in. An opt-in overlay composes the bridge, the band, and the
three example mods for a source launch.
The bridge keeps one band per session: ui.render is raised through the
mods in load order, the tree's buttons keep their onPress host-side behind
per-drawing action ids, a $.state read during a render subscribes the band
to that slot, and $.ui.open/close/invalidate, the end of a tool.call chain,
and turn.complete redraw it. ClaudeCodeMods is a TypertRemoteService:
watchBand streams snapshots, pressBand runs a button and redraws. null is
an accepted hook answer (a surface drawn empty).
A mod is now a plugin built with defineMod() around its register(on,
options); the bridge is the ClaudeCodeMods service (ctx.claudeCodeMods)
that keeps the loaded mods and raises their events. The plugin.json /
hooks.json loader, pluginDirs, and dynamic imports are gone.
prompt.submit context becomes text blocks appended to the human's own
prompt message and $.prompt.submit queues a user-sourced message, so the
bridge adds no message source kind: the persistence record and the
regenerated schema are removed and the artifacts match master again.
Engine correctness from the review round: next() is memoized and every
started run beneath is awaited before a hook's answer settles; a rewritten
tool argument skips the hook with a report pointing at the pre-tool input
rewrite proposal; timers belong to the session whose event started them;
$.tool.call injects a tool's deferred contexts; $.clock.sleep honors the
event signal; a built-in tool answered with { result } is validated against
its output schema before it becomes an error-shaped result; hooks on events
this host never raises are reported at load.
Examples: the three mods from the Claude Code blog post under examples/,
Token Weather and its test unchanged, Blast Radius and Replay Theater
completed from the published fragments, run by their own tests through a
claude-code/testing alias over the new test kit ($.ui.mount/find/press).
Review follow-up on the watchdog rewrite:
- Register the deadline reaction inside `next()` and detach it when the
demand settles. Racing against one watchdog-lifetime promise kept every
settled race reachable through that promise's reactions, retaining each
resolved `StreamChunk` for the length of the stream: a 10,000-value probe
kept 100/100 sampled values before this change and 0/100 after.
- Take the neighboring stall case's 1,000ms idle budget in the new
transport-recovery case: the retry performs a real local HTTP/SSE round
trip, and a 50ms budget would let coverage-lane scheduling decide it.
- Document the contract where it is described: `packages/util/timeout/README`
(both languages) and the bounded-recovery and timeout-library notes no
longer claim that only the transport observes the watchdog signal.
- prompt.submit sees and rewrites only the human's own messages, gains the
rewritten text on an image-only prompt, and keeps only typed result fields
- a mod's $.tool.call reaches only the mods loaded before it, attributed to
the caller, instead of re-entering every mod through the pipeline
- a .catch handler shares the failed hook's beneath state so nothing beneath
runs twice; a late next from an abandoned handler is ignored
- hook results are validated before use; detached runs report rejections
- session.end keeps $.state readable until its hooks settle; agent creation
cancellation abandons a waiting session.start hook; agents is read lazily
- a success a hook marks isError becomes a failure; replacement entries clear
on tools/result; per-agent registrations drop with the agent
- $.store writes are serialized per plugin and bounded in UTF-8 bytes;
$.http.fetch bodies are bounded; timers refuse rescheduling after unload
and unload awaits running callbacks; regex matchers reset lastIndex