Skip to content

fix-wave 1a: autoplan compaction, /cso redaction + OrbStack, browse tab recovery, zsh Bun floor - #3202

Closed
garrytan wants to merge 14 commits into
capy/fix-wave-2026-10-15from
capy/fixwave-1a
Closed

garrytan wants to merge 14 commits into
capy/fix-wave-2026-10-15from
capy/fixwave-1a

Conversation

@garrytan

Copy link
Copy Markdown
Owner

Fix wave 2026-10-15 stack (review per layer; merge only the wave PR capy/fixwave-release → main, which carries the single v1.91.104.0 stamp, the combined CHANGELOG, and runs the paid gate once). Order: capy/fix-wave-2026-10-15 (= main 47d639a) ← 1a ← 1b ← 2 ← 3 ← 4 ← release. This PR: 1a. Layer PRs target the layer below, so no paid evals run on them; they get closed with a pointer once the wave PR merges.

Summary

Fix wave 2026-10-15, layer 1a (bottom of the capy/fix-wave-2026-10-15 stack): recovery, cross-platform. It contains items 1, 5, 6, 7 and 11 of docs/designs/FIX_WAVE_2026_10_15.md (plan revision 3, approved by Garry on 2026-10-15, autoplan run run-20261015160532), plus the shared echo sanitizer. The plan is copied verbatim in its own commit. Each item was reproduced on v1.91.101.0 (47d639a) with a test that failed before the fix. Layer 1b stacks on this branch.

Fixes #3194, #3185, #3187, #3167 and #3195.

The PR adopts three external PRs, cherry-picked with their authorship kept and release churn dropped. Our changes sit in separate commits on top.

Behavior changes

  • /autoplan keeps going after a Claude Code 2.1.300 auto-compaction. lib/claude-journal-records.ts follows the compact boundary's preserved segment (fix(autoplan): own the conversation after a Claude Code 2.1.300 compaction (#3194) #3196), so the phase guard no longer answers every phase entry with close_incomplete.
  • Gate-integrity edit to the /autoplan phase guard. Owned records can end up cut off from the session root after the walk. When a compact_boundary record explains that cut-off part, it now maps to the guard's existing unrecognized_shape disposition: allow, with a visible warning and a decision-log row, under the detail unrecognized_shape:compaction_unreachable.
    • Orphaned and not-yet-flushed parts keep today's denial. A new negative control (unflushed boundary) still gets close_incomplete.
    • The existing denial tests still deny: autoplan-publication-guard, -hook, guard-reasons and autoplan-publication-boundary-361c.json.
  • /cso scanner JSON reports parse after redaction. For scan stdout declared as JSON, a redacted number token becomes the quoted string "<REDACTED-…>"; before, it was an unquoted marker that broke JSON.parse and produced INVALID_OUTPUT.
    • The declared format travels through scanner-executor, DockerGroup.execCapture, runProcess and the splice.
    • Version probes, stderr and undeclared or non-JSON streams are redacted as before.
    • A finding that crosses a token or structural boundary withholds the whole stream. Marker-only withholding and the cross-stream views check are kept, and the output is a fixpoint of that check.
  • /cso works with OrbStack's Docker. Docker and the watchdog spawn the trusted realpath with argv0: 'docker', which OrbStack's multicall binary dispatches on. The trust check still runs on the realpath.
  • The browse daemon recovers when every tab was closed outside browse.
    • The next command opens a blank tab and prints once: [browse] warning: every tab was closed outside browse, so this command ran on a new blank tab; open your page again with: browse goto <url>.
    • status, stop and restart never open a tab, so a closed context can still be inspected and stopped.
    • A failed recovery fails with BROWSE_TAB_UNRECOVERABLE and the restart command.
  • The Bun floor check is correct under zsh. _gstack_bun_below no longer depends on word splitting, so under zsh Bun 1.3.2 is now too-old; before, zsh accepted it.
    • A version or floor the check cannot parse returns a distinct status that every caller treats as a hold: bun-version-unreadable: … (BUN_VERSION_UNREADABLE). The bun path and its output pass through the shared sanitizer.
    • /gstack-upgrade Step 4 runs the check through bash -c, and on BUN_VERSION_UNREADABLE it stops with a reinstall fix, never bun upgrade.
    • The session-start auto-updater holds too, and its banner names the reinstall fix and #bun-version-unreadable; before, the reason would have fallen through to "git pull did not finish".
  • One echo sanitizer. lib/sanitize-echo.ts and its Bash twin bin/gstack-sanitize-echo.sh remove control characters, flatten to one line and cap at 200 bytes without splitting a UTF-8 character. The Bun hold and the browse recovery error use it.
  • New result codes, each with a docs/troubleshooting.md entry under its exact text: BUN_VERSION_UNREADABLE and BROWSE_TAB_UNRECOVERABLE. Also new entries browse-no-active-page, cso-scanner-invalid-output and cso-orbstack-argv0, plus the unrecognized_shape:compaction_unreachable paragraph in docs/autoplan-guard-troubleshooting.md.

Tests (each failed on 47d639a first)

Item Test On 47d639a On this branch
1 (#3196 + ours) test/autoplan-compaction-preserved-segment.test.ts. Ours: a three-compaction case, a 50k-record history inside the hook budget (~310 ms against 1000 ms), and an unflushed-boundary negative control #3196's file fails 2/7 ("records written after the boundary stay owned"); with our cases 8 of 10 fail 10/10 pass
5 test/cso-json-redaction.test.ts: the plan's number, negative, exponent, array, NDJSON, whitespace and in-string cases; the real version-then-scan capture with stderr interleaved returns status: 'complete' 6/9 fail (JSON Parse error: Unrecognized token '<') 9/9 pass
6 (#3188 + ours) test/cso-docker-argv0.test.ts. Ours: a 30 s timeout on the python3 spawnSync, and a production image probe through the multicall binary 4/5 fail (Expected: 0 Received: 127) 5/5 pass
7 (#3179 + ours) browse/test/tab-external-close-dispatch.test.ts drives the real dispatch: status, text, goto and stop after an external close, plus the closed-context case. Also #3179's browse/test/tab-external-close.test.ts 5/5 fail ("No active page") 5/5 and 3/3 pass
11 test/bun-version-shells.test.ts: one version table across bash, bash --posix, zsh and the TypeScript below(); a source check that Step 4 invokes bash 7/10 fail (zsh 1.3.2: Expected "too-old" Received "ok") 10/10 pass
11 (auto-updater) test/session-update-stages.test.ts "unparseable Bun (bun-dev (local build)) holds with BUN_VERSION_UNREADABLE…" fails (the update goes ahead) 8/8 pass
sanitizer test/sanitize-echo.test.ts under bash, bash --posix and zsh new module 20/20 pass

Intentional pin changes, each named in its commit:

  • test/autoplan-compaction-preserved-segment.test.ts (fix(autoplan): own the conversation after a Claude Code 2.1.300 compaction (#3194) #3196's own file): the three "fails closed" cases (tail-only segment, missing preserved metadata, foreign preserved uuid) and the "without compactMetadata" case now expect unrecognized_shape:compaction_unreachable instead of close_incomplete or a silently cut transcript.
  • test/cso-scanner-release.test.ts: the source pin capture(['/bin/cat', plan.outputPath]) becomes capture(['/bin/cat', plan.outputPath], plan.format). This is stricter: the report read must also carry the declared format.
  • test/session-update-stages.test.ts: "supported or unparseable Bun … proceeds" is split in two. 1.3.14 still proceeds; bun-dev (local build) now holds with BUN_VERSION_UNREADABLE. The fixture also installs bin/gstack-sanitize-echo.sh beside the floor helper, as a real install of this release does.

No other existing assertion was weakened, skipped or renamed.

Coordinator-exception note

Validation

  • bun run test: passed on the frozen head 045a398 on Ubicloud (ubirun-fixwave1a-1792096388-b746080e, standard-16, destroyed afterwards). 17/17 shards, 28,243 tests: 28,152 passed, 91 skipped, 0 failed.
    • An earlier full run, before the last two fixes, found the two failures behind the cso-scanner-release and session-update-stages pin changes above. Both pass now.
  • gstack-pregate: PREGATE: requires-remote (4 uncleared) tree=cc9557b41347 head=045a398f31d7 checks=6, run on a Ubicloud standard-16 (ubirun-fixwave1apg-1792096388-c59bdd0a) with the tree clean before and after. The local lane passed (780 files, exit 0); the same lane timed out after 3300 s on the 4-core Capy machine. The preflight passed; secrets is warn with 40 MEDIUM non-credential findings (test fixtures: phone, IP and card numbers). The uncleared remote obligations are cso-macos-launcher (6 tests), macos-named-regressions (6), cso-windows-launcher (1) and the Windows free shard (675).
  • Other checks:
    • bun run typecheck reports zero errors; bun run typecheck:test reports none new against the 865-diagnostic baseline.
    • test/spawnsync-timeout-tripwire.test.ts, test/module-size-ratchet.test.ts and test/parity-suite.test.ts pass (browse/src/server.ts is at 2223 lines against its 2224 cap).
    • bun run format:cso:check passes; gen:skill-docs output is fresh for all hosts; the troubleshooting-anchor and doctor-components checks pass.
  • Version: no stamp on this layer. The wave's top release layer carries the single VERSION stamp and the combined CHANGELOG; this lane's draft is .context/fixwave-changelog-A.md (copy at ~/.capy/work/changelog-A.md).

Paid checks (none run)

bun run eval:select (PR profile, dependents mode) selects 108 behavioral probes and 24 quality judges. The new and changed result-code and sanitizer files each select all 244 dependent gate cases; the slice plan is 81 cases in 15 slices, and 136 behaviors are deferred to release coverage. gstack-eval-plan price-gate for bun run eval:bg:pr printed GATE_PRICE: total=unknown (EVAL_GATE_UNPRICED), because the base policy declares no price_file. No paid spend happened. CI's E2E Evals workflow runs the paid PR gate when this PR opens.

What was not verified

  • The macOS and Windows checks that only CI can clear (see the pregate line) were not run here: cso-macos-launcher, macos-named-regressions, cso-windows-launcher and the Windows free shard.
  • The OrbStack fix was verified with a fake multicall binary, not a real OrbStack install. The zsh fix was verified with Linux zsh, not macOS's.

Open in Capy

garrytan and others added 14 commits October 15, 2026 17:17
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…y dispatches

executable() returns the canonical path, and OrbStack installs docker as a
symlink to docker-tools, which dispatches on argv[0] and exits 127 with
'unsupported argv0 "docker-tools"'. Every Docker call failed, so doctor
reported local-docker-isolation and every scanner missing and setup could
not preload the qualified images.

runProcess takes an argv0 option; docker.ts routes every Docker call
through runDocker, which passes 'docker'. The native watchdog's execve
calls pass "docker" as argv[0] too, so its container cleanup works on the
same installs. The executed file is still the verified canonical path.

Fixes #3187
…inary

A compiled fake that refuses any argv[0] but docker stands in for
OrbStack's docker-tools. Both behavior tests fail on the previous code.

Refs #3187
…gh OrbStack's multicall docker (#3187)

Follow-up to #3188 by Bryan Stone (@aegixx):
- the python3 spawnSync that binds the fake daemon socket gets a 30 s
  timeout; without it test/spawnsync-timeout-tripwire.test.ts fails.
- reviewer test: dockerExactImagePresent, a production probe, admits an
  image when the endpoint's canonical executable is a multicall binary
  that refuses any argv0 other than "docker". It fails on 47d639a
  (returns false) and passes with #3188.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…ction

Claude Code 2.1.300 keeps a compaction's last records in place and lists
them in compactMetadata.preservedMessages. The segment's tail, which is
also the boundary's logicalParentUuid, is written after the boundary
under the compact summary, so the parent links form a cycle that the
owned causal order never reaches. Every record after the compaction was
dropped, and the publication guard denied each later phase entry with
close_incomplete.

Such a boundary now continues from the last preserved record that is a
node of the file and not its own descendant. Boundaries without that
metadata, with a foreign preserved uuid, or with nothing kept in place
still supply no ownership.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… unrecognized_shape (#3194)

Follow-up to #3196 by vilano93. After the ownership walk, an owned node
the walk never reached was silently dropped, and the phase guard answered
the next phase entry with close_incomplete, which the user cannot satisfy.
When the unreachable node's ancestry ends in a cycle through a compact
boundary's logical parent, ownedCausalOrder now returns
unrecognized_shape:compaction_unreachable, the guard's existing
unverified disposition (allow with a visible warning and a guard-log
row). Orphans whose ancestor is missing (not flushed yet) and other roots
keep today's behaviour, so the fail-open surface does not widen beyond
compaction. Each node is walked once.

Gate-integrity edit: a close_incomplete denial becomes an unverified
allow for that shape only. Negative controls kept: the existing guard,
hook, guard-reasons and captured-fixture tests still deny (356 pass), and
a new case with the boundary not yet flushed still denies close_incomplete.

Intentional pin changes in #3196's own test file: the three "fails
closed" cases (tail-only segment, missing preserved metadata, foreign
preserved uuid) and the "without compactMetadata" case now expect
unrecognized_shape:compaction_unreachable instead of close_incomplete or
a silently cut transcript.

Reviewer tests: three compactions in one session stay owned and pass the
next phase entry; a 50k-record history with three compactions is decided
in ~300 ms (budget 1000 ms), so descends needs no memo.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
lib/sanitize-echo.ts and bin/gstack-sanitize-echo.sh flatten CR/LF/TAB to
spaces, remove every other ASCII control byte (ESC included) and DEL,
collapse runs of spaces, and cap the result at 200 bytes of valid UTF-8
with no leading or trailing space. Every external string gstack echoes
into a message, a log line or a key=value state file goes through it
(fix wave 2026-10-15, plan rev 3 DX F8). test/sanitize-echo.test.ts runs
both twins on one table under bash, bash --posix and zsh, including a
"\nstages=" injection, ESC sequences and UTF-8 characters cut by the cap.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…able version (#3195)

Reported by mochafreddo. _gstack_bun_below word-split $_gb_core, which
zsh does not do: `[ -ne ]` errored and every error path read as "not
below", so /gstack-upgrade Step 4 accepted Bun 1.3.2 in a zsh tool shell.

- _gstack_bun_below copies the release's parsed fields into quoted
  positional parameters and compares with $((10#...)); no new _gb_*
  globals. It returns 2 when either side cannot be parsed.
- gstack_bun_status prints malformed for an unparseable version or floor
  (never ok); gstack_bun_incoming_hold holds on it with
  bun-version-unreadable: ... (BUN_VERSION_UNREADABLE), the bun path and
  its output passed through gstack_sanitize_echo. setup's own malformed
  warning is unchanged.
- /gstack-upgrade Step 4 runs the check through `bash -c` and stops on
  BUN_VERSION_UNREADABLE with a reinstall fix (never "bun upgrade").
- New result code BUN_VERSION_UNREADABLE with its troubleshooting entry;
  bin/gstack-doctor-components.sh regenerated from the table.

test/bun-version-shells.test.ts: one version table (1.3.2,
1.3.3-canary.1, 1.3.3, 1.3.14, 1.4.2, 1.10.0, 01.04.02, 2.0.0, garbage)
drives bash, bash --posix, zsh and the TypeScript twin's below();
`zsh -c '. bin/gstack-bun-version.sh; gstack_bun_status 1.3.2'` printed
ok on 47d639a and prints too-old now.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…3187)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
Reported in #3185. The scanner path splices redactions into raw JSON, and
redactFindingSpans replaced a bare 10+ digit number
("max_memory_bytes":1145427776, an epoch) with an unquoted marker, so
JSON.parse failed and every non-trivial Semgrep run was INVALID_OUTPUT.

- lib/redact-engine.ts: redactJsonFindingSpans, a string- and
  escape-aware lexical tokenizer over one or more top-level values
  (NDJSON included). A span inside a number replaces that whole token
  (-, fraction, exponent) with the quoted marker, number by number; a
  span inside a string keeps the in-string marker, widened to whole
  escapes; a span touching several tokens, whitespace, a literal or a
  structural character returns null (withhold). Non-JSON text gets the
  plain splice. redactFindingSpans is unchanged for every other caller.
- The scan operation's declared format rides the real capture path:
  scanner-executor (scan stdout, or the report file read for outputPath
  plans) -> DockerGroup.execCapture -> runProcess -> spliceRedaction,
  which redacts the concatenated stdout once and maps each interleaved
  run onto it. Version probes and stderr stay plain; marker-only
  withholding and the cross-stream views check are kept, and the output
  is a fixpoint of that check.

Behavior change: a redacted number in a scanner report is now the string
"<REDACTED-...>" instead of an unquoted marker; non-JSON streams keep the
unquoted form (test/cso-eval.test.ts unchanged).

Intentional pin change: test/cso-scanner-release.test.ts pinned the
report read as capture(['/bin/cat', plan.outputPath]); it now pins
capture(['/bin/cat', plan.outputPath], plan.format), so the read also
carries the declared JSON format (stricter, not weaker).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
Closing the last tab outside browse (for example, closing the headed
Chromium window) left the daemon wedged: the page 'close' handler removed
the tab and set activeTabId to 0, and executeCommand called
getActiveSession() first, so every later command (goto, status, even stop)
failed with "No active page" until the daemon PID was killed by hand.

closeTab() already opens a blank tab when the last tab closes; the external
close path never did. Add BrowserManager.ensureActiveTab(), which reselects
a remaining tab or opens one blank tab (concurrent callers share one
recovery), and call it before resolving the active session in
executeCommand.

Fixes #3167
… says what it did (#3167)

Follow-up to #3179 by aftabkh4n, which reopens a tab on demand.
- BrowserManager.ensureCommandTab runs before every command: status,
  stop and restart never open a tab, so a daemon whose browser or context
  is gone can still report and be stopped; any other command gets an
  active tab. A failed recovery is BROWSE_TAB_UNRECOVERABLE carrying the
  restart command (cause passed through sanitizeEcho), and a command that
  ran on a newly opened tab prints a one-line warning first.
- server.ts reads the session inside the read and write branches instead
  of before dispatch (2223 lines, cap 2224) and exposes browserManager
  and handleCommandInternal on __testInternals__ for the dispatch test.
- meta-commands.ts: handleMetaCommand no longer reads the active session
  before its switch (only snapshot and resume used it), and status prints
  getCurrentUrl() so it works with zero tabs. This file is outside the
  lane's listed ownership; no other lane or #3190 touches browse/.
- New result code BROWSE_TAB_UNRECOVERABLE and troubleshooting entries
  for it and for the old "No active page" wedge.

Reviewer test browse/test/tab-external-close-dispatch.test.ts drives the
real dispatch in a child process: status, text, goto and stop after an
external close, and the closed-context case. On 47d639a (with only the
test seam added) all five fail with "No active page".

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…Stack's argv0 refusal (#3185, #3187)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…lds the auto-update (#3195)

Item 11 makes every caller of gstack_bun_incoming_hold treat a Bun
version it cannot read as a hold (bun-version-unreadable,
BUN_VERSION_UNREADABLE). The session-start banner only knew
bun-too-old, so the new reason fell through to "git pull did not
finish ... gstack is not updating" with a pull-and-setup fix that does
not help.

- bin/gstack-session-update: a bun-version-unreadable banner case with
  the reinstall fix and the troubleshooting anchor; the comment that
  said a malformed version passes is corrected.

Intentional pin change: test/session-update-stages.test.ts "supported
or unparseable Bun (bun-dev (local build)) proceeds: setup decides" is
split. 1.3.14 still proceeds; bun-dev (local build) now holds with
BUN_VERSION_UNREADABLE, leaves the checkout and setup untouched, and
the banner names the reinstall fix. The fixture installs
bin/gstack-sanitize-echo.sh beside the floor helper, as a real install
of this release does. The new case fails on 47d639a.

Ownership exception granted by the coordinator for Lane A
(bin/gstack-session-update is Lane B's file; PR 1b stacks on 1a).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
@capy-ai

capy-ai Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Merged to main through the wave PR #3209 (squash 3e8b34d, v1.91.106.0), which carried this layer unchanged. Closing the layer PR.

@capy-ai capy-ai Bot closed this Oct 16, 2026
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.

4 participants