Skip to content

journeys: the action fuzzer (correctness and speed) as a mode of the smoke-gate driver - #29071

Merged
cmux-bot[bot] merged 49 commits into
feat-cmux-nextfrom
cp/journeys-action-fuzz
Oct 31, 2026
Merged

cmux-bot[bot] merged 49 commits into
feat-cmux-nextfrom
cp/journeys-action-fuzz

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The action fuzzer for cmux NEXT, built as a mode of the journeys / dogfood smoke-gate driver (cmux-ad 2026-10-31: "we need to fuzz greater for both correctness and speed"; fuzz-catalog rows a and m). One action layer and one set of oracles with the smoke gate.

  • Brings scripts/journeys (the journeys runner and the dogfood smoke gate, from cp/pa-d-dogfood-smoke-gate at 1222849c09f, owner agent a7031071e77d3e582) to feat-cmux-next. Today it lives only on perf/chrome-parity and that branch.
  • scripts/journeys/ext_fuzz.py: step fuzz_run. It plays a seeded plan of user actions through debug.key (the key router's real path), ext_window's drags and the driver's relaunch, at mixed timings (now, pair within 60 ms, slow), with a model of workspaces, tabs, terminals and panes. Oracles after each burst: create (or a visible refusal), r3a and r3b through ext_smoke's own cond_smoke_r3, _widths and _columns, focus, topology (daemon == app), stall (> 100 ms in cmux code), crash, and leak after close-all. A failure is shrunk in the same app with ddmin. Speed marks per create: shown_ms and typeable_ms.
  • scripts/journeys/fuzz.py: rounds through run.py (exclusive, pool perf-lead, never cmux17), a --base A/B on the same mini (ABBA), confirmation of each shrunk repro in a fresh lease, written as a regression journey. Verdicts: CLEAN, RED, NOT JUDGED, DIAGNOSTIC. A .clean file is written only for a judged clean run.
  • Action sets: layout (row m: New Column, Cmd-D, Split Down, Ctrl-, close pane, divider drag, window resize, relaunch) and full`.
  • journeys/fuzz/: the fuzz journey, and Lawrence's reported bugs as fixed sequences (lawrence-*.json).

This is not a CI job, step or gate (Lawrence 2026-10-23). It runs on the fleet, started by the lead, the tip watcher and the dogfood flow.

Testing

  • python3 scripts/journeys/test_fuzz.py: 16 OK (plan, bursts, ddmin, the speed table, and the verdicts with a stand-in run.py).
  • python3 scripts/journeys/test_fuzz_sim.py: 8 OK. The oracles run against a simulated app. A correct app plays clean. Each planted bug fails with its own oracle (second split lost, column resize, grid mismatch, PTY mismatch, a stall in cmux code, a PTY leak), and ddmin shrinks it.
  • python3 scripts/journeys/test_smoke_gate.py: OK. run.py --check: 59 valid.
  • python3 scripts/cmux-next/prepush-gate.py: no new violations.

Observed / Not run

  • Observed: the Python tests above, on the agent VM.
  • Fleet runs (diagnostic, exclusive perf-lead leases): the layout set on feat-tip-fixes-v2 (f2f3083bac80ab71101d03ac), v2.2.11-opt, v2.2.12-opt (152ca95f959989d8d6c8c250) and v2.2.10-opt. It found an R3b red (New Column narrows the old column 872 -> 291 pt, feat-tip-fixes-v2) and an R3a red (a stale fit_grid after Cmd-D, on v2.2.10-opt and v2.2.12-opt). Both are saved as red journeys in journeys/fuzz/found-*.json. feat-tip-fixes-v1 cannot be driven: it is a plain Release build with no debug.* methods.
  • Not run: whole-host (key window) mode. It needs the Cua grant on cmux17.

Performance

No perf numbers: this changes only harness Python under scripts/journeys (no app, daemon or web code). The fuzzer's own speed marks (shown_ms and typeable_ms for each create, an A/B against --base) are results it reports about the builds it tests. They are not a performance change of this PR.

Changelog

none


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Adds a model-based action fuzzer for cmux NEXT as a mode of the journeys smoke-gate driver, bringing the journeys runner and smoke gate from cp/pa-d-dogfood-smoke-gate with the fuzzer built on top.

The fuzzer (ext_fuzz.py)

  • Plays seeded plans of user actions through debug.key, ext_window's drags and the driver's relaunch at mixed timings, with oracles for create, keys, R3a/R3b, focus, daemon-vs-app topology, stall, crash and leak.
  • The create oracle counts new ids, not net counts, so a close landing after the baseline never hides a create.
  • A split_type burst runs Cmd-D (or Split Down) with a line typed at once, then a second split 30 ms later with its own line; each must land in its own new pane or it is a keys red (TIP RED 1193).
  • On builds with debug.creation.hold/release, the hold is proven by the release reply, so n(hold) = n(rounds); a split the app refused with a visible message is not judged.
  • A fuzz_crash_reports step classifies a mini's crash reports as product or harness.

The orchestrator (fuzz.py)

  • Rounds run through run.py on exclusive perf-lead leases (cmux19 only by default), with an optional --base ABBA; every lease waits on the mini scheduler's go hand-off, where one GO is one lease and a repeated GO is dropped.
  • Desync, stall and focus are recorded findings, not stops; only a finding unique to the build is RED, and signatures drop UUIDs, window ids and long hex.
  • Shrinking runs in separate short replay leases, and a --replay is shrunk only when --shrink-tries is given.
  • Verdicts are CLEAN/RED/NOT JUDGED/DIAGNOSTIC; CLEAN needs --base, positive --minutes, full fuzz time and no replay, and writes a .clean file.
  • Normal mode requires --whole-host with a key window; without one it reports "not run: known gap", never a product red.
  • Signals are blocked until the lease is inside its try and through clean-up, so a stop never kills the lease release; rounds from an unpinned harness checkout are "not run: harness unpinned".

R3b: New Column keeps every old column's exact width; Cmd-D and Split Down split inside a column without changing others.

Includes test_fuzz.py (33 tests), test_fuzz_sim.py (24 tests, 50 sim seeds clean), test_run_release.py (6 tests, including LateLease and DoubleStop), test_run_stale.py, test_run_stale_result.py and test_smoke_gate.py, all runnable without a fleet or app.

Written for commit f9037b6. Summary will update on new commits.

View guided diff Turn on auto-fix

…uzz.py, fuzz.py)

Brings scripts/journeys (the journeys runner and the dogfood smoke gate, from
cp/pa-d-dogfood-smoke-gate at 1222849) to feat-cmux-next, and adds the
action fuzz on top of it (cmux-ad 2026-10-31, fuzz-catalog rows a and m):

- ext_fuzz.py: step fuzz_run plays a seeded plan of user actions through
  debug.key, ext_window's drags and the driver's relaunch, at mixed timings,
  with a model of workspaces/tabs/terminals/panes. Oracles after each burst:
  create (or a visible refusal), R3a and R3b through ext_smoke's own
  smoke_r3 / column readings, focus, daemon == app topology, stall > 100 ms
  in cmux code, crash, leak after close-all. A failure is shrunk in the
  same app with ddmin. Speed marks per create: shown_ms, typeable_ms.
- fuzz.py: rounds through run.py (exclusive, pool perf-lead, never cmux17),
  --base A/B on the same mini (ABBA), confirms each shrunk repro in a fresh
  lease and writes it as a regression journey; verdict CLEAN/RED/NOT
  JUDGED/DIAGNOSTIC, .clean file only for a judged clean run.
- Sets: layout (row m: New Column, Cmd-D, Split Down, Ctrl-`, close pane,
  divider drag, window resize, relaunch) and full.
- journeys/fuzz/: the fuzz journey and Lawrence's reported bugs as fixed
  sequences (lawrence-*.json).
- test_fuzz.py (plan, ddmin, speed table, verdicts with a stand-in run.py)
  and test_fuzz_sim.py (the oracles against a simulated app with planted
  bugs): python only, no fleet.

Tests: python3 scripts/journeys/test_fuzz.py (16 OK),
test_fuzz_sim.py (8 OK), test_smoke_gate.py (OK), run.py --check (59 valid).
@lawrencecchen lawrencecchen added the risk:low Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 45ddfc2: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr29071 -f ref=cp/journeys-action-fuzz -f platform=macos -f backend_mode=local

Shipped in:

@github-actions github-actions Bot added risk:normal Risk tier (docs/ci/risk-tiers.md) and removed risk:low Risk tier (docs/ci/risk-tiers.md) labels Oct 31, 2026
@blacksmith-sh

blacksmith-sh Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
test_every_listed_hook_is_one_glaeda_released_with_the_check (main.SigningHooksInSt
epWithGlaedaTests.test_every_listed_hook_is_one_glaeda_released_with_the_check)/
test_every_listed_hook_is_one_glaeda_released_with_the_check (main.SigningHooksInSt
epWithGlaedaTests.test_every_listed_hook_is_one_glaeda_released_with_the_check)
View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, Claude): BLOCK at 0224106

Scope: git diff 1222849c09f HEAD -- scripts/journeys shows only the new fuzz files, the one-line run.py RESULT_FILE change (fuzz.json) and the README section. The copied gate files are equal to 1222849. I ran test_fuzz.py (16 OK), test_fuzz_sim.py (8 OK), test_smoke_gate.py (33 OK) and run.py --check (59 valid). I also ran small probes against the PR's own FakeApp and the stand-in for run.py. I built nothing.

BLOCK

  1. A false CLEAN when a round fails outside the fuzz oracles. In scripts/journeys/fuzz.py:174 and :179-186, summarize looks only at failure (from fuzz.json) and at status == "not run". A round with status: "fail" and no failure is skipped. A round gets that status when:

    • the expect step after fuzz_run fails (fuzz_daemon_agrees, smoke_frame);
    • smoke_ready fails;
    • fuzz_run raises before it writes fuzz.json. For example, j.key raises when the app dies in the middle of a burst (ext_fuzz.py:385). step_window_drag (:427) and step_relaunch (:443) also raise uncaught StepErrors, and a relaunch where the app does not quit is a real app bug.

    That round has seconds: None, so the loop plays more seeds until the budget is met, and the verdict is CLEAN. I reproduced it with the test's stand-in for run.py: {"jobT": {"status": "fail"}} gives verdict CLEAN and writes jobT.clean. The fix: count status == "fail" with no fuzz failure as RED, or at least as NOT JUDGED. Also catch StepError in play so that a crash in the middle of a burst reaches the crash oracle and fuzz.json.

  2. A false RED from predict on a correct app (--set full). In ext_fuzz.py:516-518, a workspace create does not reset want["panes"]. delta counts panes only for the workspace shown at the end (:546). Take a burst split_right, split_right, new_workspace: it predicts panes +2, but the new workspace has 1 pane. I reproduced it on the sim's correct FakeApp: FUZZ create ... wanted {... 'panes': 2}, got {... 'panes': 1}. 10 of the first 50 full-set seeds (300 actions each) contain this burst. The fix: set want["panes"] = 0 when the shown workspace changes (on Cmd-N, and on Ctrl-` from Home).

  3. A false RED from paste followed by type_line (--set full). paste (ext_fuzz.py:435-439) pastes fzp<nonce><n> into the focused terminal and does not press Return. A later probe in the same terminal (:617) then runs fzp...echo fz... $(stty size). That is "command not found", so the probe ran on 0 terminals and gives a keys red (:638-639). In 46 of 50 full-set seeds, a type_line comes within 3 actions after a paste. The sim cannot catch this because it sets the paste weight to 0 and its FakeApp ignores Cmd-V. The fix: after the paste, clear the line (Ctrl-U), or press Return and check that the pasted text arrived. Then the paste is also checked.

Notes (not blocking)

  • R3b docstring vs code (ext_fuzz.py:27-29 vs :695-701): the docstring says 0.5 pt, but the code uses 0.01. It also says "on a tree screen every pane except the one split keeps its width", but on a tree screen every pane has column: None, so _widths gives one union extent and R3b checks nothing there. Change the docstring, or implement the per-pane check.
  • Reuse: r3a calls ext_smoke.cond_smoke_r3 directly (:668), which is good. r3b reuses _widths and _columns, but it copies the matching loop of cond_smoke_same_widths (:695-701) instead of calling it. It drops the gone/added checks on purpose. That is a small parallel copy.
  • r3b is read once and not through settle_check (:847-848), unlike r3a and topology. A layout change still in flight can give a red.
  • After a relaunch, hang_seq = 0 (:841) counts every launch-time hang record of the new process. Cold-launch stalls over 100 ms with a cmux frame will give a red on a relaunch. Confirm that this is what you want.
  • Shrink tries append to fz.speed (:858), so a failing round's A/B samples include the replays.
  • shown_ms for workspace and tab_page uses done_at (:585), which includes the wait for the other creates' prompts in the burst. typeable_ms means "any non-blank text" (:570), not "a prompt".
  • Verdict edges in fuzz.py:183: CLEAN needs only 60 % of --minutes (the docstring says "at least --minutes"). --minutes 0 writes a CLEAN file after zero rounds. --replay with a small --minutes can write CLEAN. Without --base, CLEAN is written with no speed A/B at all, but the README usage shows --base as required.
  • ddmin (:213-238) is a correct ddmin. fails matches only the oracle name (:927), so it can shrink to a different bug of the same oracle.
  • reset() closes only workspaces that are not in the baseline. If a baseline workspace shows at launch, the layout set changes it, and shrink tries do not start from the same state.
  • The probe output (about 17 cells) can wrap in a very narrow pane after many splits. Then the probe finds 0 holders (:628), unless the app refuses splits below a minimum width.
  • Fleet safety is OK: nothing sends signals (a relaunch is Cmd-Q on the app it launched), cmux17 is excluded by default (JOURNEYS_EXCLUDE_HOSTS, and --host cmux17 is refused), and run.py releases the leases. pbcopy writes the mini's pasteboard, which is acceptable under an exclusive lease.
  • Python 3.9 is OK: ext_fuzz.py parses with feature_version=(3,9) and has no match, no runtime X | Y and no 3.10+ APIs. It does not import driver.py. It imports ext_smoke lazily.

- A round whose journey failed outside the fuzz verdict (smoke_ready, the
  final expect's other checks, a harness error) is RED, never skipped.
  An action whose input path fails is recorded and judged by the oracles
  (crash first) instead of ending the step.
- predict: a workspace create resets the pane count (delta counts the
  shown workspace only).
- paste: a comment line plus Return, so the next probe's line is clean.
- desync, stall and focus are recorded findings (once per signature, with
  the seed and the actions before), not stops: the first trial on
  v2.2.11-opt and v2.2.10-opt stopped at the first Cmd-N on desync W1/W4/G1
  and a 190 ms CefInitialize stall on both builds. A finding only on the
  build (not on the base) is RED; one on both is a base finding.
- R3b read through settle_check; launch stalls after a relaunch skipped;
  shrink replays add no speed samples or findings.
- Verdict: CLEAN needs --base, --minutes > 0, the full fuzz time and no
  replay; cmux20 (the quiet bench host) out of the default hosts.

Tests: test_fuzz.py 19 OK, test_fuzz_sim.py 9 OK, 80 sim seeds clean.
… every run and shrink try

- R3b: New Column (Ctrl-Cmd-D) keeps every old column's exact width; Cmd-D
  and Split Down split inside a column and no OTHER column changes width.
  Cmd-D is no longer a column action. New Column always starts its burst,
  so a red names the action that changed the column.
- The layout set starts each run and each shrink try with every workspace
  closed and one fresh Ctrl-` workspace: the first fleet runs' shrink tries
  started in a workspace an earlier try had split, so ddmin could not shrink.
- Corpus lawrence-column-widths: New Column twice, Cmd-D, Split Down, a
  New Column pair.
- Sim: inside splits; planted 'a split resizes another column' is r3b, a
  split alone is not.

Tests: test_fuzz.py 19 OK, test_fuzz_sim.py 12 OK.
@cursor

cursor Bot commented Oct 31, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, Claude): BLOCK at 12857a6

Scope: git diff 0224106ddd0 12857a69b80 -- scripts/journeys. This includes the R3b-scope and fresh-start commit, which was pushed while I did this review. I also read the whole fuzzer where the delta touches it. I ran the tests on a git archive of 12857a6, because the worktree has uncommitted edits: test_fuzz.py OK (19), test_fuzz_sim.py OK (12), test_smoke_gate.py OK (33), run.py --check 59 valid. I built nothing.

The three earlier BLOCK items are fixed

  1. A false CLEAN from a round that fails outside the fuzz verdict. journey_problems (fuzz.py:111) and broken (fuzz.py:202) make it RED: a failed step, an expect check other than fuzz_clean, or status: fail with no failure and no findings. play (ext_fuzz.py:425) records input errors, and a relaunch that fails gives crash (:884). If state() raises after the app dies, fuzz.json is not written, and the round is BROKEN, so the verdict is RED.
  2. predict sets want["panes"] = 0 on Cmd-N and on Ctrl- from Home (ext_fuzz.py:551, :558). This is correct with delta`, which counts only the workspace shown at the end.
  3. paste now sends : fzp… and then Return (ext_fuzz.py:468-471). The probe line after it starts clean.

The new verdict rules are correct. CLEAN needs --base, --minutes > 0, at least one action, the full fuzz time, no replay and no diagnostic (fuzz.py:220-230). Shrink replays add no findings or speed samples. In the new commit, split_column exempts only the column a Cmd-D or Split Down splits inside, and only when no New Column comes earlier in the burst. When a New Column starts its burst, every old column must keep its exact width. That agrees with the 08:06Z scope. The fresh precedence (:381, A or (B and C)) gives fresh for the layout set only, which is what the comment says. Python 3.9: the file parses with feature_version=(3,9), uses no 3.10+ APIs and does not import driver.py. Fleet safety did not change: no signals, cmux17 excluded, cmux20 not in DEFAULT_HOSTS (fuzz.py:50).

BLOCK

  • A false RED: signature keeps window UUIDs (ext_fuzz.py:979-984). The focus detail starts with window {id} (:761). Window ids are lowercase UUIDs (WindowManager+Membership.swift:300). The regex changes only the digits: window 3fa85f64-… becomes focus:window #fa#f#-#-#-b#fc-#c#f#afa#: …. The base and the build are different apps, so their window ids are different. The same focus mismatch on both builds therefore gets two signatures, on_base is False, and fuzz.py:220 gives RED. I reproduced it: summarize on one test round and one base round, each with the same focus finding in a different window, gives RED [on_base False]. This is likely, not rare. DebugFocus.mismatches uses the W1 expected responder, and the first trial saw W1 on both builds. Desync W5 has the same problem ("AppKit key window is ", InputInvariants+World.swift:109). The fix: replace UUIDs and long hex runs ([0-9a-f]{8}-[0-9a-f]{4}-…, \b[0-9a-f]{12,}\b) with *, or remove window <id> from the signature. Add a test that has two window ids.

Notes (not blocking)

  • paste presses Return without a check of the focused tab. After Cmd-T, the text goes into the New Tab field, and Return submits it (NewTabPage+Open.swift:310). Depending on the selected kind, the text runs as a terminal command, a web search or an agent prompt. An agent tab on a fleet mini can also change the process counts that leak uses. Press Return only when the focused tab is a terminal, the same check that probe does.
  • The base comparison is per signature, with no allowance for chance. A stall near the 100 ms limit, or a finding that the base never reached because its round stopped on a STOP failure, gives RED. This is by design, but say it in the README.
  • After a relaunch, hang_records(j, 0) reads at most 200 records (limit). If the new process has more records than that, hang_seq can be too low.
  • When an action errors because the app died, creates runs before crash, so failed() reports create and not crash. The verdict is still RED.
  • The new commit sets fresh only from fuzz_set (ext_fuzz.py:381). The --corpus round passes no fuzz_set, so the layout corpus does not get the fresh start. In fresh mode, if the app keeps one workspace open, close_all waits its full 15 s at the start of each run and of each shrink try. This does not change the verdict.

…ures); the first red repro

- signature() drops UUIDs, window ids and long hex: one focus or desync
  finding on the build and the base is one signature (no false RED).
- paste only into a focused terminal (Return would submit a New Tab field).
- The corpus round runs in the chosen set (the layout corpus gets the
  fresh start).
- journeys/fuzz/found-r3b-new-column-narrows.json (P0, red on
  feat-tip-fixes-v2 f2f3083bac80ab71101d03ac): Cmd-N, Ctrl-`, New Column;
  the old column went 872 -> 291 pt. Owner a03ef38e4dc40283d.

Tests: test_fuzz.py 20 OK, test_fuzz_sim.py 12 OK, run.py --check 60.
… Cmd-N, Ctrl-`, Cmd-D; fit_grid 110x40 vs grid 36x40)
@cursor

cursor Bot commented Oct 31, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, Claude): SHIP at c70020e

Scope: git diff 12857a69b80 c70020ece26 -- scripts/journeys (ext_fuzz.py, fuzz.py, test_fuzz.py, two new journeys). I ran the tests on a git archive of c70020e (scripts/): test_fuzz.py OK (20), test_fuzz_sim.py OK (12), test_smoke_gate.py OK (33), run.py --check 61 valid. I built nothing and tested no app.

The BLOCK is fixed

  • signature (ext_fuzz.py:986-993) first replaces UUIDs in either case with <uuid>, then window_/window: ids, then hex runs of 12 or more characters. I checked it with real detail shapes: the focus line window <uuid>: ... model pane <uuid> (:768) and desync W5 AppKit key window is window("<uuid>") / childPage(window: "<uuid>") (InputInvariants+World.swift:109). Each gives one signature for two different window ids. 0x… addresses are not changed, because the hex rule needs a word boundary. The new regex does not catch Swift selectors in stall stacks (windowDidBecomeKey:, NSWindow). There is a test: test_signature_drops_window_ids_and_uuids (test_fuzz.py:105).

Other checks

  • Paste guard (ext_fuzz.py:466-467, :490-493): when the focused tab is not a terminal, the action is skipped. This is the same check that probe does. A state() error goes to the play error path. Nothing reads pasted, so a skipped paste cannot change a verdict.
  • Corpus round (fuzz.py:333) now passes fuzz_set. The layout corpus therefore gets the fresh start (ext_fuzz.py:381-383).
  • found-r3a-split-fit-grid-stale.json and found-r3b-new-column-narrows.json are valid. Each has a fixed 3-action sequence with set: layout, shrink: false and budget_s: 120.
  • Python 3.9: both files parse with feature_version=(3,9). ext_fuzz.py imports only stdlib and does not import driver.py. Fleet safety did not change (DEFAULT_HOSTS fuzz.py:50, the cmux17 guard at :311).

Notes (not blocking)

  • The two repros were shrunk in corpus rounds that ran before this commit, when those rounds did not use the fresh start. In set: layout, the replay first closes every workspace, and ensure_target puts a Ctrl- before Cmd-N (ext_fuzz.py:973). Thus the journey plays 4 actions, not the 3 in why`. This is deterministic, but confirm that each repro is still red in this form.
  • topology cuts desync detail to 200 characters before signature (:799). A UUID cut at character 200 is not replaced, so it could still split one signature into two. This is rare.
  • The [_:][0-9a-zA-Z-]+ rule merges tab:terminal and tab:browser, but no detail string has that shape today.

… per terminal, on in replays; Cmd-N first needs no Ctrl-` set-up
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, Claude): SHIP at 488ca50

Scope: the delta c70020e..488ca50 (scripts/journeys/ext_fuzz.py, scripts/journeys/fuzz.py). I did not build or test the app.

Verdict checks (no blocker found):

  • Probe holder detection (ext_fuzz.py:700-716) uses re.fullmatch(line + " (\d+) (\d+)") on each row. The typed trap line (trap 'echo FZW $(stty size)' WINCH) and its FZW r c output lines cannot match it. The probe tag read (ext_smoke._pty_size, ext_smoke.py:474) needs a 3-token row whose first token is the tag, so FZW rows do not change the PTY size for R3a. R3b reads view frames only, so the trap has no effect on it.
  • Tracing is off by default. Only fuzz.py --replay sets fuzz_trace (fuzz.py:329), and no journey file sets it. When tracing is off, trace_sizes and arm_winch return at once.
  • ensure_target (ext_fuzz.py:1014): a burst that starts with Cmd-N now gets no Ctrl-` set-up. Cmd-N makes a workspace on Home, so this is correct.
  • Python 3.9: both files parse with ast.parse(..., feature_version=(3,9)). stdlib only. ext_fuzz.py does not import driver.py. The ' key is in DebugKey's map.
  • No fleet-safety change.
  • From a git archive HEAD of scripts/: test_fuzz (20), test_fuzz_sim (12) and test_smoke_gate (33) pass, and run.py --check passes (61 journeys).

Non-blocking findings (these affect traced replays only):

  1. ext_fuzz.py:937: arm_winch types the trap line and Return before the probe checks that the focused tab is a terminal (ext_fuzz.py:695). If the focus is on a browser or agent pane, a replay types into it. Fix: move the call into probe after that check.
  2. fuzz.py:328-329: a replay now uses --set (default layout) and no longer uses the journey step's full. A full-set repro that is replayed without --set full thus gets a fresh close-all and Ctrl-set-ups. This agrees with the seed and corpus rounds. Suggestion: readset` from the repro file when it has one.
  3. ext_fuzz.py:604: this catches only D.StepError. A subprocess.TimeoutExpired from the CLI (30 s) escapes creates and settle_check.
  4. Every poll now does 3 more reads, so replay timing is not the same as the original run's. No unit test covers trace_sizes, _trace or arm_winch.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Oct 31, 2026
…ad 09:05Z)

- run.py: SIGTERM raises Terminated, so run_lease's finally releases the
  lease; an acquire stopped by a signal is ended (capture-host unreserves).
- run.py: the build comes from a per-mini cache outside the slot
  (~/.cache/cmux-journeys/builds/<job>/, newest 3), filled atomically by
  the first lease of that job; later rounds download nothing.
- fuzz.py: ddmin runs outside the round, one short replay lease per try
  (--shrink-tries 10); the in-app shrink is off by default. fuzz.py passes
  its own SIGTERM to run.py as TERM (never KILL) and waits.
- test_run_release.py: release after an error inside the round, after
  SIGTERM while driving, the acquire ended after SIGTERM while acquiring,
  and fuzz.py's SIGTERM reaching run.py as TERM.

Tests: test_run_release.py 4 OK, test_fuzz.py 20 OK, test_fuzz_sim.py 12 OK,
test_smoke_gate.py OK, run.py --check 61.
@lawrencecchen lawrencecchen removed the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Oct 31, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, Claude): BLOCK at 22115f3

Delta reviewed: 488ca50..22115f3 (scripts/). Tests passed on a git archive of HEAD with Python 3.12.3: test_run_release (4), test_fuzz (20), test_fuzz_sim (12), test_smoke_gate (33), and run.py --check (61 files).

Blocker: a lease leak when fuzz.py is stopped with ^C (SIGINT to the process group)

  • fuzz.py:202-204: on the KeyboardInterrupt, fuzz.py sends TERM to run.py. run.py got the SIGINT too and is already in the finally of run_lease. The TERM raises Terminated inside subprocess.run([ch, "release", lease]) (run.py:411). subprocess.run then kills the release child.
  • I reproduced this with the real run.py, a fake capture-host whose release takes 3 s, and a parent that does what Runner.lease does. After SIGINT to the group, the log shows release-start and never shows release-done. If the TERM comes earlier (during the hog stop or before finally), the release does not run at all. The lease then stays until it expires (about 10 min). A single TERM, or a TERM to the whole group (the two signals merge into one), releases correctly; I checked both.
  • Fix: block SIGINT and SIGTERM (signal.pthread_sigmask) for the cleanup in finally (hog stop and release), then unblock. Or make the handler raise only once.
  • The same applies to two small windows. First, acquire_lease (run.py:283): communicate has returned the lease, and a signal arrives before the function returns. Second, run.py:310-317, before the try. In both cases the lease is printed but never released. One masked region from the acquire to the release closes all three windows.

Not blocking

  • run.py:348: c={cache} puts the job id in unquoted, and rm -rf "$c" uses it. A --build .. or an id with shell characters would be a problem. Check the id against [A-Za-z0-9_-]+.
  • Pruning sorts by directory mtime. touch "$c/ok" on a hit does not change it, so the order is by creation, not by use. Stale *.tmp.* folders from stopped downloads are never removed (about 1 GB each). Two non-exclusive slots that download the same job at the same time: the second rm -rf "$c" can delete the first one's bundle before its launch clones it. Result: not run, not a wrong verdict. fuzz.py and the gate take exclusive leases, so this cannot occur for them.
  • On a cache miss, the download still happens while the lease is held.
  • run.py:297-299: the HOG_WARMUP_SECONDS comment moved to the BUILD_CACHE_KEEP line.
  • Checked and correct: the ddmin is bounded (--shrink-tries plus one confirm for each signature), no try stops the search with None, confirmed is set only when that exact sequence failed with the same oracle, nonlocal k is valid, the gate verdict does not change (a Terminated run gets "not run"), ext_fuzz.py keeps 3.9 syntax, driver.py is not changed, and capture-host accepts a bundle in ~/.cache and reads capture-commit beside it.

… paths, unpinned on disk, cut tar, edit while packing (red)

Review SHIP with should-fixes. Five new cases, red on 3592cf3
(python3 scripts/journeys/test_run_stale.py: 4 failures, 1 error): a used
--out keeps the old <id>/ and run.json in place; a refused result's folder
stays at <id>/ (clip and checks point at it); an unpinned round's result.json
still says pass and fuzz.json stays; a cut-short result tar raises out of
run_lease; an edit between the state read and the kit pack counts as pinned.
…, unpinned on disk, cut tar is not run, state read after packing

- A used --out: the earlier results.jsonl, run.json and <id>/ folders are
  renamed with this run's id (finding 1).
- A refused result.json: its <id>/ folder becomes <id>.foreign-<run id>, so no
  clip, check frame or reader takes it (finding 2).
- Unpinned: result.json on disk gets the not-run status and reason too, and
  fuzz.json becomes fuzz.json.unpinned (finding 3).
- A cut-short or bad result tar (tarfile.TarError, OSError) is "not run",
  not an exception out of run_lease (finding 4).
- No start commit is unpinned, not a TypeError (finding 5); pinned: None on
  rows with no lease (finding 11).
- The harness state is read before and after kit_tar; a change between them
  is unpinned (finding 6).
- A journey start before the run start is logged, not refused: the random
  run id decides (finding 7).
- Docs: what is packed at each lease, and whose id the .earlier name has
  (findings 9, 10).

Green: python3 scripts/journeys/test_run_stale.py (13 tests).
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, Claude): SHIP at 72d5c43

Scope: delta 996108d..72d5c43 in scripts/journeys (commit 72d5c43: a --replay is never shrunk again).

Findings:

  • A replay failure stays RED. summarize() builds fails from the rounds, not from the shrink loop, so the verdict, the oracle and the detail do not change. I ran the new test's scenario and read summary.txt: RED, FAIL create ... split_right+split_right: no terminal, no refusal, exit 1.
  • No crash on replay rounds. The code after the loop reads only r.get("shrunk"|"shrink_leases"|"confirmed"|"repro") (summarize) and f['shrunk'] or [] / f.get(...) (text_of). shrunk comes from the round's report, so the replayed sequence still shows in the summary.
  • No repro is lost that matters: the replay input is already the repro. One edge case: a bare-list replay input no longer becomes a journey file. That is acceptable, because the input file is already the reproducible artifact.
  • Non-replay runs do not change. The new condition not opts.replay is constant for the run, and for non-replay runs the loop body is the same as before.
  • The new test is a real regression test. I removed and not opts.replay from a copy: test_replay_is_never_shrunk FAILED. With the fix it passes.
  • Nit (cosmetic, no block): for a replay, text_of still prints (confirmed in a fresh lease: None; repro None). A follow-up could print "replay (this is the confirmation)" there.

Observed / Not run

Observed, in a git archive HEAD scripts | tar x copy: test_fuzz.py (31 OK), test_fuzz_sim.py (24 OK), test_run_release.py (6 OK), test_smoke_gate.py (33 OK), run.py --check (62 journey files valid). The replay summary output was as described above. The new test fails without the fix.
Not run: no app build, no fleet lease, no real replay on a mini.

…a pinned replay of the 3-action R3b repro on cmux20 shrank to New Column alone: shrinking a replay can be worth its leases)

Test: the default replay uses one lease; --shrink-tries 4 shrinks it. test_fuzz.py 31 OK.
… aside with or without result.json; a failed folder rename is logged, not raised

Re-review of 7115d71 (SHIP) nits 1 and 2. Nit 3 (a cut tar with a complete
result.json) is left: dogfood-gate checks the frames itself.
Green: test_run_stale.py (13), test_run_stale_result.py, test_smoke_gate.py (65).
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, Claude): SHIP at f487d34

Delta reviewed: 72d5c43b993..f487d3489f2 -- scripts/journeys (fuzz.py, test_fuzz.py).

Findings:

  • Default resolution is correct. --shrink-tries is None until the shrink stage. Then it becomes 0 for --replay and 10 for other runs. The ddmin bound in fails() reads opts.shrink_tries after this resolution, so it gets the resolved value. No other code reads opts.shrink_tries before line 512.
  • Normal runs still shrink by default. test_failure_is_red_shrunk_confirmed_and_written passes (shrunk to 1 action, shrink_leases > 0, confirmed). I also ran it with the fake runner myself: 5 leases with the default.
  • --shrink-tries 0 on a normal run does not crash. I ran it with the fake runner: RED, exit 1, 4 leases. The summary has shrunk from the round report, and shrink_leases/confirmed/repro are None. summarize uses r.get, and text_of uses f['shrunk'] or [] and f.get('confirmed'). Both handle the None values.
  • test_replay_is_never_shrunk proves both replay cases: the default makes 1 lease, and --shrink-tries 4 makes more than 1 lease and shrinks to 1 action.

Non-blocking notes:

  • A change in behavior: before this delta, --shrink-tries 0 on a normal run still did the confirm lease and wrote repros/<id>.json. Now it skips both. This agrees with the new replay semantics, but no test covers it. A short note in --help would make this clear.
  • No test does --shrink-tries 0 on a non-replay run, and no test asserts the exact default of 10 (the tests only assert > 0).
  • The test name test_replay_is_never_shrunk is now wrong. It should be something like ..._is_shrunk_only_when_asked.

Observed: all from scripts/journeys of a git archive HEAD scripts copy. test_fuzz.py 31 OK, test_fuzz_sim.py 24 OK, test_run_release.py 6 OK, test_smoke_gate.py 33 OK, run.py --check 62 journey files valid. I also did ad hoc fake-runner runs with the default and with --shrink-tries 0.
Not run: no app build and no fleet runs.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Oct 31, 2026
@lawrencecchen lawrencecchen removed the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Oct 31, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, Claude): BLOCK at 5be1587

Reviewed the merge result git diff f487d3489f2 5be1587efe714f1c96de6835a25c82e79f5dc3c7 -- scripts/journeys, focused on how it fits with the fuzz code.

Blocking: a false RED from an unpinned round. run.py stamp_harness (scripts/journeys/run.py:320-327) changes the round's status to "not run" in result.json and renames fuzz.json to fuzz.json.unpinned. It does not change the steps or the evidence in result.json. Then fuzz.py journey_problems (scripts/journeys/fuzz.py:115-138) reads those steps with report=None. The fuzz_run skip at fuzz.py:130 needs report.failure, so the fuzz's own failing step counts as a "problem". summarize (fuzz.py:291) puts the round in broken, and the verdict is RED (fuzz.py:313-314). A failing smoke_ready step, or a failing fuzz_daemon_agrees check, in an unpinned round also gives RED. I ran a simulation: stamp_harness with a dirty harness on a round with a fuzz_run failure, then journey_problems and summarize with a base round. Result: status "not run", report None, journey_problems ['step 3 (fuzz_run): ...'], verdict RED. Expected verdict: NOT JUDGED.
Fix: journey_problems should return [] when result.json's status is "not run" (or when harness.pinned is false), plus a test in test_fuzz.py for an unpinned failing round.

Checked, OK:

  • Signal mask (run.py:456-468, 475, 578-593): intact after the merge. harness_state/kit_tar (run.py:481) run inside the try with signals unblocked, so a TERM raises Terminated and finally releases the lease. Each git call has a 60 s timeout. The late-lease release in acquire_lease, the phases and the build cache did not change.
  • Shrink and confirm (fuzz.py:516-536): a None report gives fails() -> None and confirmed=False. A None report is never counted as a failure.
  • HARNESS_PATHS (run.py:85) covers every file kit_tar packs: driver.py, ext_*.py (incl. ext_fuzz.py), smoke_hog.py, journeys/, build_names.py, pr_media_ocr.swift.
  • Tests all pass (git archive copy): test_run_stale (13), test_run_stale_result (1), test_run_release (6), test_fuzz (31), test_fuzz_sim (24), test_smoke_gate (33), run.py --check (62 journey files).

Nit: set_aside_old_results (run.py:459) can raise OSError while signals are blocked. The BaseException branch restores the mask and re-raises, so no lease is held, but run.py crashes. The other errors there become "not run".

…ogy; the app's desync monitor stays a recorded finding (it broke r11 round 2 on cmux20, seed 71008: a G1 desync counted twice made the round 'broken', a false RED)

Test test_daemon_agrees_does_not_count_a_desync_finding_again: red on
5be1587, green here.
…ine, known gap) gives no journey problems: NOT JUDGED, never a false RED (review of 5be1587, BLOCK)

Test test_unpinned_round_is_not_judged_not_broken: red on d3a25bb,
green here. test_fuzz.py 32 OK.
…O is dropped, and each .done names the GO it used (scheduler STATUS 1590: r11's shrink-001-02 on cmux20 at 12:54:53Z leased on the previous GO's copy)

Test test_a_repeated_go_is_one_lease_and_done_names_its_go. test_fuzz.py 33 OK.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, Claude): SHIP at 560419a

Reviewed the delta git diff 5be1587efe7 560419a2b99 -- scripts/journeys (d3a25bb, 560419a).

The BLOCK is fixed. journey_problems (scripts/journeys/fuzz.py:126-129) returns [] when result.json says "not run" or harness.pinned is False. I ran a simulation: real run.stamp_harness with a dirty harness on a round that has a fuzz_run failure, a smoke_ready failure and a failed fuzz_daemon_agrees, then journey_problems and summarize with a base round. Result: status "not run", no problems, no failure, verdict NOT JUDGED (it was RED at 5be1587).

Pinned rounds still give RED (same simulation): smoke_ready failed, frame check failed, fuzz_run harness crash with no fuzz.json, and fuzz_daemon_agrees failed with a topology problem. A deadline round and a known-gap round (D.NotRun, ext_fuzz.py:402, with a failed step recorded) give no problems, count in not_run, and give NOT JUDGED.

(1) does not hide topology. topology() (ext_fuzz.py:1004-1023) gives "topology" for a failed cmux terminal list and for terminals the app shows but the daemon does not have. It gives "desync" only for debug.desync violations. RECORD = {"desync", "stall", "focus"} (ext_fuzz.py:136), so the filter at ext_fuzz.py:1302 removes only desync. Every burst records desync through split (ext_fuzz.py:1160, 1196), and fuzz_clean judges it.

Tests: test_fuzz.py:370 and test_fuzz_sim.py:511 fail on the 5be1587 fuzz.py/ext_fuzz.py and pass now. All suites pass: test_run_stale 13 (fresh worktree, now removed), test_run_stale_result 1, test_run_release 6, test_fuzz 32, test_fuzz_sim 25, test_smoke_gate 33, run.py --check (62 journey files). fuzz.py and ext_fuzz.py parse with ast.parse(feature_version=(3,9)). The machine has no 3.9 interpreter, so I only did a syntax check.

Nits (do not block):

  • fuzz.py:116-118: the journey_problems docstring does not say that a not-run round gives [].
  • ext_fuzz.py:1302: a desync that first shows after the last burst's check (between the end of fuzz_run and the final expect) is now in no verdict. With no action played in that gap, this is not likely.

@cursor

cursor Bot commented Oct 31, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, Claude): SHIP at 45ddfc2

This receipt replaces my SHIP at 560419a (#29071 (comment)). All of that review still applies: the unpinned round gives NOT JUDGED, pinned smoke_ready, frame, fuzz_run crash and topology failures give RED, deadline and known-gap rounds count as not run, and ext_fuzz.py:1302 filters only desync.

Delta 560419a..45ddfc2 (one GO is one lease):

  • A go file with no "go" key still works (scripts/journeys/fuzz.py:182-183): stamp is None skips the repeat check. I ran two leases in a row on go files with no key. Both leased, last_go stayed None, and the .done has "go": null.
  • opts.last_go is set (fuzz.py:199) before wait_go returns, so Runner.lease calls _lease and then signal_done (fuzz.py:211) with that value. Expired or wrong-host go files do not change it. fuzz.py runs leases one at a time, so there is no race on opts.
  • A repeat GO is unlinked and dropped. A new GO is then taken. A lease cannot take one GO two times.
  • test_fuzz.py:317 fails on the 560419a fuzz.py (no "go" in .done) and passes now.

Runs at 45ddfc2 (git archive copy): test_run_stale 13 (fresh worktree, now removed), test_run_stale_result, test_run_release, test_fuzz, test_fuzz_sim, test_smoke_gate all OK. run.py --check: 62 files valid. fuzz.py parses with ast.parse(feature_version=(3,9)).

Nits (do not block): the journey_problems docstring (fuzz.py:116-118) does not say that a not-run round gives []. The wait_go docstring (fuzz.py:160-164) does not give the repeat-GO rule.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Oct 31, 2026
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent October 31, 2026 17:35 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent October 31, 2026 19:43 Inactive
@cmux-bot
cmux-bot Bot merged commit 3de731a into feat-cmux-next Oct 31, 2026
122 of 126 checks passed
@cmux-bot
cmux-bot Bot deleted the cp/journeys-action-fuzz branch October 31, 2026 20:08

This branch was previously deployed

1 inactive deployment
ci-routing-agent — f9037b6f Deployed Oct 31, 2026 by cmux-bot[bot] via cmux-tui tree preflight #91180
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants