Repository navigation
acpmux: a prompt right behind another is accepted queued at once (fuzz g1) - #29175
Conversation
…pted until the first turn ends (fuzz row g, g1)
… (fuzz g1) A prompt that took the turn lock holds the chat before start_turn shows its turn (start_turn runs after the awaits of check_dispatch, family_dispatch_check and hold_retry_start). queue_slot read only the turn and the queue count, so a second prompt in that window did not queue: it waited on turn_lock for the first prompt's whole turn and heard no _acpmux/prompt_accepted. With a long turn the sender got no answer at all. Now the prompt that takes the lock for its turn marks it (Session::turn_claimed, set by ClaimedTurn with the lock and cleared before the lock is released). queue_slot counts that mark as a turn: the second prompt is journaled (its queued record) and accepted at once, queued, with its position, and its turn's records follow when it starts. The count is read before the mark, and a prompt takes the mark before it leaves the count, so no prompt passes between them. An automatic continuation never queues, so it still waits for the lock and check_cut decides after, as before. The acceptance also says what happened, additively: state queued, started or steered (docs/acpmux-schema.json). An older client reads queued and steer as before. Red: fuzz_g1_a_prompt_right_behind_another_is_accepted_within_two_seconds at the test commit. New: fuzz_g1_a_prompt_right_behind_another_is_accepted_queued_next.
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Shipped in:
|
|
Review (claude-review-subagent): FIX at 31eeaa9 Read-only review of Findings
Behavior changes a person can see (intended, I think; please confirm in the PR description)
All three match what any other queued prompt does, so they are not defects. |
|
Found 3 test failures on Blacksmith runners: Failures
|
…s its real place Review (FIX at 31eeaa9) found the g1 race still open: queue_slot's check whether a prompt must wait and its increment of the queued count were two steps, so two prompts of one chat could both find it free; one then waited on turn_lock with no prompt_accepted. Both now run under one leaf lock (Session::admit), held for no await. The acceptance's position is now where the prompt went into the queue (1: next), as an inbox digest already names its own, not the count, which a prompt about to start its turn is still in. A deterministic regression test (prompt_accept_tests.rs) holds the first prompt at prompt_start_seam, after it took the lock and before its turn shows, and requires the second prompt's acceptance, queued at position 1, with its queued record in the journal, while the first still waits. first_work_tests: two agent messages queued while the first work is about to start both join its turn now (turn_count 1, was 2): each queues behind the claim, as one behind a shown turn does.
|
Review (claude-review-subagent): SHIP at 31771d8 I re-reviewed the repair delta Both earlier findings are fixed
I found no new defect. Notes (they do not block the merge)
|
|
Fuzz row g (owner of rows f/g/h) on head 31771d8:
Limit: this is one run. Catalog rule 5 says that a flake stays red until it passes 5 of 5. The g1 stall came in 6 to 8 of 20 trials on feat, so one green run of 12 seeds is strong evidence, but it is not 5 of 5. I run it 4 more times and add the result here. |
|
Row g repeats on a585103 (= 31771d8 + row g harness), for the 5-of-5 rule:
|
|
g2 replays (filter fuzz_g_crash_durable_seed_a, 5 each): base 400b94d + harness (553aa64): jobs 9cef87e9, 6a1b0f18, 089600ff, 4b30f680, 17792454: lost-draft 0 of 5, g1 stalls in 4 of 5 (expected at the base). Head 31771d8 + harness: jobs accf47b9, 69185296, 4f897bac, 83b8a51e, 7ba0438d: 0 violations in 5 of 5. g2 is a rare race (1 of 11 head runs, 0 of 5 base runs), so these replays do not show where it is. By code reading (STATUS 1179) the cause is a lost update between two save_meta calls of one chat (hub/mod.rs:1867, store.rs:1411), which this PR does not touch. A deterministic fault-point red test on feat is in progress; it gives the proof. |
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Summary
Fuzz finding acpmux g1 (fuzz row g, seed 26370; STATUS 1052): a prompt that comes just behind another prompt in the same chat got no
_acpmux/prompt_accepteduntil the first prompt's whole turn ended. With a long turn, the sender got no answer at all. In the app this reads as a lost message: the composer keeps the text, and Enter and Send do nothing. No durable answer was lost.Cause: a prompt takes
turn_lockinqueue_slot, but its turn shows insession.turnonly later, instart_turn, after the awaits ofcheck_dispatch,family_dispatch_checkandhold_retry_start.queue_slotread only the turn and the queue count, and its check and its count increment were two steps. So a second prompt in that window (or one that came at the same time) did not queue. It waited onturn_lock.lock().awaitfor the first prompt's whole turn, with no acceptance.Fix (acpmux,
hub/queue.rs,hub/subsession_runs.rs,hub/session.rs):Session::turn_claimed, set byClaimedTurnwith the lock, cleared before the lock is released).queue_slotcounts that mark as a turn.queuedcount are one step under a new leaf lock,Session::admit(no await under it; order admit, queue, turn; onlyqueue_slottakes it). Two prompts that come together never both find the chat free.queuedrecord) and accepted at once withqueued: true, state: "queued", position: n. Its turn's records follow when its turn starts.positionis now the prompt's real place in the queue (insert index + 1, so 1 = next), as an inbox digest already reports its own. Before, it was thequeuedcount, which a prompt about to start its turn is still in. The journal'squeuedrecord carries the same value.after_turn) never queues, so it still waits for the lock andcheck_cutdecides after, as before. Ano_queuesubagent retry still getsretry.busy.stateisqueued,startedorsteered(docs/acpmux-schema.json). An older client readsqueuedandsteeras before; the webview (direct.ts), the Swift pane, cmux-tui-core and the CLI read onlypromptIdandqueued. A duplicate or inbox acceptance sends nostate.No client timer and no client-side guess: the daemon answers when the prompt is in its journal. The fuzzer's 2 s oracle is unchanged.
Behavior changes (a prompt sent while the earlier prompt is still starting is now a queued prompt, so it acts as any other queued prompt does):
first_work_testsnow expectsturn_count1).Clients: the app's agent pane already draws a queued prompt as a queue row from the
queuedrecord (no number by design, Leo 2026-10-21), so it needs no change: the fix makes the record and the acceptance exist in this race. The CLI (acpmux send --no-wait) printsqueuedas before.Testing
All runs are on the Linux fleet (
cmux-ci run --class light --script scripts/ci/cmux-tui-rust-check.sh). Nothing was built on a developer machine. PR head:31771d801ee41836bf5efc8cf9edcdbc6b008ba1. Each job names its exact commit. Two commits are scratch trees, never merged:c8988bc0992c96c8974a02f9dccb15ca59ed0691(fuzz-red/acpmux-g1-unit) = the red test commit12e2f3f1ef6+prompt_accept_tests.rsonly.a585103a3733c5eb1760c32a3ef0ec964df73721(fuzz-red/acpmux-g1-fix-31771d80) = the PR head31771d801ee+ the 2 row-g harness commits of test(acpmux): fuzz row g, kill -9 crash fuzz of durable answers #29103 (1673bda5a4f,a585103a373; they add onlytests/fuzz_crash_durable.rsand.config/nextest.toml).test fuzz_g112e2f3f1ef61bbcd50598ad7f8fe29c12bd8492f(test commit)fuzz_g1_a_prompt_right_behind_another_is_accepted_within_two_seconds,FUZZ-G1 silent=14 of 20; control passestest fuzz_g131771d801ee41836bf5efc8cf9edcdbc6b008ba1(head)test fuzz_g1_(fuzz owner)31771d801ee41836bf5efc8cf9edcdbc6b008ba1(head)test prompt_accept_testsc8988bc0992c96c8974a02f9dccb15ca59ed0691(scratch: base + test)prompt_accept_tests.rs:61"the second prompt is accepted before the first turn ends: Elapsed"test prompt_accept_tests31771d801ee41836bf5efc8cf9edcdbc6b008ba1(head)clippy31771d801ee41836bf5efc8cf9edcdbc6b008ba1(head)test fuzz_g_crash_durable)a585103a3733c5eb1760c32a3ef0ec964df73721(scratch: head + harness)a585103a3733c5eb1760c32a3ef0ec964df73721(scratch: head + harness)test31771d801ee41836bf5efc8cf9edcdbc6b008ba1(head)test(base)400b94dba51f5bf321cb257e165cb1b652404dce(base)10 of the 11 head fails also fail at the base (
cmux-tui::setupx6,cli_audit,peer_restart,startup_load,subsessions_safety).daemon_list_guard::clients_asking_for_the_whole_list_over_and_over_cost_the_daemon_a_few_percentfailed once at the head and passed on the solo rerun in the same job: a CPU-budget flake, not in the queue path.Earlier head, superseded (for the record only):
31eeaa9f154greenfuzz_g1job ce7a383f04fe5a957d7335cc (3 of 3).Observed / Not run
Observed: the fleet runs above, red and green, each on its own assertion.
Not run: a live app check in a tagged build (the pane code is unchanged). The
admitstep has no deterministic red of its own (the unit test drives the claim path); the 20-trial socket test and fuzz row g cover it. The fuzz owner adds the remaining row-g repeats (5 of 5) as comments, each ona585103a373(head + harness).Changelog
Fixed: A prompt sent just behind another one in an agent chat shows as queued at once, not after the first turn ends
Proof
No capture: daemon change only; the pane's queue row is unchanged.