feat(hub): native-resume recipes as shared data, and one resume dispatch (N1) - #533
Merged
Conversation
…tch (N1) Pane-state-manifests plan, N1 — the wedge the review pass in #532 said to take first because its consumer (vision-parity L3) is already scheduled. ## The table New `hub/internal/resumerecipes`: 17 engine CLIs and how each reattaches to a prior session, plus a mapping from our agent families onto a resume MECHANISM (argv / acp_session_load / appserver_thread_resume). Sixteen rows transcribed from herdr `src/agent_resume.rs` @ 6f311498 (v0.8.0, Apache-2.0 — NOTICE updated); the `gemini` row is ours, grounded in driver_exec_resume.go. It ships as YAML, not Go literals, because the declared consumer is the desktop Companion's local agent service in Electron main. It cannot call a Go table, only re-derive one, so a Go-only N1 would have guaranteed the second copy it exists to prevent. `testdata/resume_recipes_fixture.json` is generated from the table and compared byte-for-byte on every run, so the file going stale IS the signal that the two languages may disagree — the recipe L2 shipped in #526. ## Accuracy, recorded per row claude / codex / agy were verified by running each binary's `--help` on this host; `gemini` is verified by our own driver; the other 13 are marked `vendored` and a test fails if one is promoted to `probe` without someone running it. Vendored reference DATA gets less review than vendored code and fails silently downstream, so the grade lives in the data rather than being averaged across the file. Two mappings are refused on purpose. `kimi-code-ts` is NOT wired to herdr's `kimi --session` — nobody has confirmed it is the same binary as our compiled-TS CLI, and a wrong recipe does not error, it cold-starts. `gemini-cli` is `acp_session_load`, not `argv`: the hub injects the ACP field, and the `--resume` the M2 driver uses is threaded per turn from a cursor it captured itself. Calling that an argv family would have made the rewired splice rewrite a cmd the hub has never rewritten. ## What the rewire found The plan said the ad-hoc knowledge lived in driver_exec_resume.go / teleport respawn. It didn't. driver_exec_resume.go is correctly driver-internal and is untouched; the real site was resume_splice.go plus TWO hand-copied family switches — and they had already diverged. The spec-mutation copy never grew antigravity's arm. That is latent, not live: step 2 of that path returns errUnknownFamilyField for any family absent from flagForField, and antigravity is absent. But adding antigravity to flagForField would have silently turned every mode/model flip into a cold start. Both call sites now share one `spliceResume`. Also latent and now fixed: `kimi-code` (the retired Python family, #378) still has a live arm for rows persisted before its retirement. An enumerator listing spawnable engines would have dropped it and silently cold-started those sessions; it has a row, and a test pins every family the hub splices for. ## Shell injection engine_session_id is captured verbatim from the agent's own session.init payload (captureEngineSessionID validates nothing) and spliced into backend.cmd, which tmux runs through a shell. It was joined unquoted, so `x; rm -rf /` would have executed at the next resume, respawn or teleport. Values now pass a validation envelope and are quoted only when they need it, so every id that was already safe renders byte-identically and no live spawn spec changed. An id quoting cannot rescue is refused and the respawn cold-starts — the fallback that path already takes everywhere else. The quoting is checked against a real `sh`, not against my model of one: the test replaces the binary with `printf` and asserts the shell hands the id back byte-identical. Mutation-checked — swapping to double quotes passes the structural check and FAILS the shell check on `$(…)` and backticks, so both seams are live and independent. Not done: the dedupe rule is a primitive (`DedupeKey`, tested), not a policy. Nothing consumes it, because there is no single restore-pass owner to hang "one ref resumes once" on. That needs identifying first. Verification: full `go test ./...` exit 0; 11 runnable CI lints pass (lint-openapi needs a venv this machine lacks); 17 pre-existing splice tests pass untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CodeQL flagged `make([]string, 0, len(tokens)+2)` in rewriteResumeFlag as an overflowable size computation. The pattern predates this PR (the old antigravity rewriter had it too), but it is newly reachable from attacker-influenced input now that the splice path validates and quotes a session id the engine supplies, so the alert is fair. The capacity is a hint — append grows past it — so the arithmetic bought nothing. len(tokens) alone, with the reason recorded at the line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rewriteResumeFlag scans cmd tokens for the engine binary and accepts a path suffix match, and specs commonly lead with `cd <workdir> &&`. A workdir that happens to end in the engine's name — `cd ~/w/claude && claude --model x` — matched on cd's operand first, splicing the resume flag onto the directory: `cd ~/w/claude --resume <id> && …`, which does not cold-start, it kills the spawn outright (`cd: too many arguments`). A token that is `cd`'s operand is a directory, never an invocation, so the scan now skips it. Pre-existing in agy's old scanner; the table rewrite generalized it to claude and every future argv engine, which is why the fix and its pin live in the shared rewriter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 8, 2026
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.
Pane-state-manifests plan N1 — the wedge #532's review pass said to take first, because its consumer (vision-parity L3) is already scheduled for W3.
What lands
hub/internal/resumerecipes— 17 engine CLIs and how each reattaches to a prior session, plus a mapping from our agent families onto a resume mechanism (argv/acp_session_load/appserver_thread_resume). Sixteen rows are transcribed from herdrsrc/agent_resume.rs@6f311498(v0.8.0, Apache-2.0 — NOTICE updated); thegeminirow is ours, grounded indriver_exec_resume.go.It ships as YAML, not Go, because the declared consumer is the Companion's local agent service in Electron main. It cannot call a Go table, only re-derive one — so a Go-only N1 would have guaranteed the second copy it exists to prevent.
testdata/resume_recipes_fixture.jsonis generated from the table and compared byte-for-byte every run, so the file going stale is the signal that the two languages may disagree. Same recipe L2 shipped in #526.Accuracy is recorded per row
probe--helpon this hostin-treevendoredA test fails if a row is promoted to
probewithout someone running it. Vendored reference data gets less review than vendored code and fails silently downstream, so the grade lives in the data instead of being averaged across the file.Two mappings are refused on purpose:
kimi-code-tsis not wired to herdr'skimi --session. Nobody has confirmed it is the same binary as our compiled-TS CLI (ADR-054), and a wrong recipe doesn't error — it cold-starts.gemini-cliisacp_session_load, notargv. The hub injects the ACP field; the--resumethe M2 driver uses is threaded per turn from a cursor it captured itself. Calling it an argv family would have made the rewired splice rewrite a cmd the hub has never rewritten.What the rewire turned up
The plan named the wrong site. It said
driver_exec_resume.go/ teleport respawn. That file is correctly driver-internal and is untouched; the real knowledge was inresume_splice.goplus two hand-copied family switches — and they had already diverged.respawn_with_spec_mutation.go's copy never grew antigravity's arm.That divergence is latent, not live: step 2 of that path returns
errUnknownFamilyFieldfor any family absent fromflagForField, and antigravity is absent, so an antigravity agent never reaches the splice. But adding antigravity toflagForFieldwould have silently turned every mode/model flip into a cold start. Both call sites now share onespliceResume.Also latent and now covered:
kimi-code, the retired Python family (#378), still has a live arm for rows persisted before its retirement. Anyone enumerating spawnable engines would have dropped it and silently cold-started those sessions. It has a row, and a test pins every family the hub actually splices for.Shell injection (please look here)
engine_session_idis captured verbatim from the agent's ownsession.initpayload —captureEngineSessionIDvalidates nothing — and spliced intobackend.cmd, which tmux runs through a shell. It was joined in unquoted (strings.Join(out, " ")), so an id likex; rm -rf /would have executed with the agent's privileges at the next resume, respawn or teleport.Values now pass a validation envelope (non-empty, no control chars, ≤512 bytes) and are quoted only when they need it, so every id that was already safe renders byte-identically and no live spawn spec changed. An id quoting cannot rescue is refused and the respawn cold-starts — the fallback that path already takes everywhere else.
The quoting is checked against a real
sh, not my model of one: the test swaps the binary forprintfand asserts the shell hands the id back byte-identical.Mutation checks
ShellQuotereturns input unchanged → hostile test fails.$(…)and backticks. Both seams are live and independent.kimi-coderow → fails in both the table test and the dispatch test.Not done, deliberately
The dedupe rule is a primitive, not a policy.
DedupeKeyexists and is tested; nothing consumes it, because there is no single restore-pass owner in our architecture to hang "one ref resumes once" on. Wiring it needs that owner identified first — a separate wedge, and I've recorded it as such in the plan rather than half-building it.Verification
go test ./...— exit 0, 32 packages.lint-openapineeds a venv this machine lacks.gofmtclean on everything I touched.Docs: new
reference/engine-resume-recipes.md, plan updated with the four as-built deviations, changelog gains Added / Security / Fixed entries.🤖 Generated with Claude Code