Skip to content

VC-807: confirm a minted harness launch before it moves the resume seed; skip announce for --version/--help/housekeeping and nested runs - #921

Merged
hussainph merged 3 commits into
mainfrom
volli/VC-807-terminal-resume-runs-the-wrong-harness-or-a-phan
Oct 19, 2026
Merged

hussainph merged 3 commits into
mainfrom
volli/VC-807-terminal-resume-runs-the-wrong-harness-or-a-phan

Conversation

@hussainph

@hussainph hussainph commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Defect

The generated harness wrapper called volli session harness <id> (with --mint for argv-id harnesses: claude-code, cursor) on every invocation inside a Volli terminal, whatever the arguments. The host treated each call as a launch:

  • a mint overwrote harnessSessionId with a fresh uuid at once ("Overwrites, always"), and repointed activeHarnessId;
  • an announce that changed the harness without a mint (codex, opencode) repointed activeHarnessId and kept the previous harness's id.

Resume reads the two fields together (pty/scope.ts), so after ordinary housekeeping commands:

  • claude --version / claude mcp list / claude doctor / claude update with seed A → Resume session ran claude --resume '<phantom>' (no conversation found);
  • codex --help (or a real codex closed before its first turn) after a claude session → codex resume '<a claude uuid>';
  • a nested claude -p / claude mcp list run by a live claude's Bash tool inherited PATH + VOLLI_SESSION, minted over the parent's seed, and (with -p) reported its own turns as the parent's.

I checked the cited lines on current origin/main (wrapper.ts announce/mint, harness-verbs.ts recordHarnessLaunch / activeHarnessId / "Overwrites, always", pty/scope.ts resume build) before changing anything.

Fix

Wrapper (packages/shared/src/harness/wrapper.ts)

  • New optional adapter field nonSessionArgv: { flags, subcommands }. A declared flag in any position (--version, --help, …), or a declared subcommand as the first word, skips the announce/mint. Nothing else about the invocation changes. The lists come from the installed CLIs' --help output: claude, codex (-V is its version flag, not -v), opencode, cursor-agent.
  • A run nested inside a running harness of the same kind (any of that adapter's own sessionMarkers is set, e.g. CLAUDECODE) execs the real binary exactly as outside Volli: no announce, no mint, no injected hooks. Volli already scrubs these markers from every terminal it spawns (scrubInheritedSessionEnv), so here they can only have come from a running harness above this process. A marker that is not a valid shell name is skipped, so the wrapper can't fail to parse.

Host (packages/host-core/src/agent-dispatch/harness-verbs.ts)

  • A mint is now pending (an in-memory per-Session map with the same 512 LRU bound as the watermarks). It writes nothing durable. The harness and the seed move together only when the minted harness fires session.started, or a hook naming the minted id. If the event reports an id, that id wins. When the record moves, a second session-harness notice with changed: true repoints the renderer caches; the mint's own notice now says changed: false.
  • An announce that changes the harness without a mint clears harnessSessionId, so resume falls back to the new harness's latest line instead of pairing a foreign id with the new binary.
  • Any new announce or mint drops an older pending launch, so a straggling hook from a launch that never proved itself can't repoint the record.
  • Kept: mint-per-launch (strict v4, never reused), the wrapper's fail-open behaviour, user-resume token handling, and the recordHarnessLaunch count on every announce/mint. The resume argv/semantics in VC-401: per-harness resume reference table, rendered from the resume builder #882's table are unchanged.

E2E fixtures

  • fake-harness.mjs: in interactive mode the fake claude now fires the SessionStart a real claude fires at boot, synchronously, before its capture loop. That keeps interrupt-resume-smoke check 5 (resume with the minted id) meaningful under the new rule.
  • harness-wrapper-smoke check 7 (quarantined, not run in CI) now asserts that unconfirmed mints do not become the seed.

How the regression tests prove it (they fail on main)

  • apps/desktop/src/main/harness-runtime.test.ts runs the real generated wrapper in a shell with a fake volli that logs every call:
    • claude --version | -v | --help | mcp list | doctor | update | install → no volli call and no --session-id, and a following real claude hello still mints (7 cases);
    • codex --help, codex login, opencode --version → no announce;
    • nested claude -p hello with CLAUDECODE / CLAUDE_CODE_ENTRYPOINT set → argv untouched (no --settings, no --session-id) and no volli call.
    • All 12 fail with the source changes stashed (verified locally) and pass with them.
  • packages/host-core/src/agent-commands.test.ts (session.harness block), checking the resume line pty/scope.ts builds:
    • seed A + an unconfirmed mint → still claude --resume 'seed-a';
    • claude seed A + codex announce → codex resume --last, not codex resume 'seed-a';
    • opencode + a claude mint → nothing moves until claude's SessionStart, then the harness and seed move together, with a changed: true notice;
    • a minted id becomes the seed once its SessionStart names it (existing behaviour kept);
    • an id-less startup event proves the launch, and a superseding announce forgets the pending mint.
    • Three of these fail on main (the seed overwrite, the premature harness switch, the foreign-seed pairing). The others pin the behaviour that is kept.
  • packages/shared/src/harness/wrapper.test.ts: text assertions for the scans, the nested gate and the invalid-marker skip. wrapper.ts and the four adapter files are at 100% under --coverage.

Decisions

  • Pending in memory, not in the durable native detail. A pending launch only matters for the second or so between the mint and the harness's startup hook. If the app restarts in that window, that one launch loses its promotion, but its hooks still write the seed they report. No durable shape change, so N−1 is untouched.
  • A launch whose hooks never fire no longer becomes the seed. This is the ticket's rule and the conservative choice: we keep the last confirmed seed rather than one Volli invented. Cost: in a terminal that switched harnesses, a harness whose hooks are broken resumes the previous harness. That case is already surfaced as a silent channel.
  • recordHarnessLaunch still counts every mint/announce. Deferring it would hide exactly the broken-hooks launch that silence detection exists for. The false silent after claude --version is fixed by the wrapper no longer calling Volli for those invocations.
  • Non-session lists are conservative and taken from each CLI's real --help. A listed word that is not actually a subcommand would be read as a prompt and start an unannounced session. So claude config, which 2.1.x no longer has, is left out, and so are the background-session verbs (attach, agents, …), codex exec/review/resume/fork/servers, opencode run/serve/web/attach/pr and cursor agent/ls/resume. Registered manifests declare none (the field is optional).
  • A nested run is passed through completely, not just un-announced: the injected hooks would otherwise report the nested run's turns against the parent Session (acceptance: "does not change the parent's seed or activity").
  • A changing announce clears the seed rather than keeping it. If opencode's boot hook ever beats its own backgrounded announce, the worst case is falling back to opencode --continue until its next hook reports the id again.

Not in this PR

  • Restricting the hook path's seed write to the running harness (a hook from any harness still writes the id it reports, as before).
  • Per-adapter non-session lists for registered manifests (no manifest schema change).
  • A broader marker check, e.g. a nested codex inside a running claude. The ticket scopes the check to the adapter's own markers, and codex/opencode/cursor declare none yet.

Local checks

  • vp check: pass
  • typecheck for @volli/shared, @volli/host-core, @volli/desktop and the root config: pass
  • pnpm check:protocol-schema --base origin/main: pass
  • node scripts/check-public-content.mjs: pass
  • packages/shared: vp test run src/harness/ src/harness-command.test.ts --coverage (scoped to the touched files): 312 passed, 100%
  • packages/host-core: vp test run src/agent-commands.test.ts src/agent-dispatch.test.ts src/agent-services.test.ts: 236 passed
  • apps/desktop: vp test run src/main/harness-runtime.test.ts (44 passed) and src/main/pty.test.ts (224 passed)

CI notes

The wrapper announced and minted for every invocation, so 'claude --version',
'claude mcp list' or 'codex --help' repointed the running harness and
overwrote the resume seed, and Resume session ran a phantom id or the wrong
binary.

- Wrapper: skip the announce/mint for adapter-declared non-session argv
  (--version/--help and housekeeping subcommands), and run a harness nested
  inside a running one of its kind (its own session markers set) exactly as
  outside Volli.
- Host: hold a minted id as pending; the harness and its seed move together
  only when that harness's startup hook, or a hook naming the id, arrives.
- Host: an announce that changes harness without a mint clears the foreign
  seed, so resume falls back to the new harness's latest.
@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 34bef397-0717-484d-8f4a-cffadeca7785
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant