Repository navigation
feat(skills): add verify-mention-queue, a TLA+ model of the @mention queue - #523
Conversation
…queue Adds .agents/skills/verify-mention-queue/: a TLA+ model of the @mention queue and the WebSocket clients it sends to (init.lua queue_mention, schedule_mention_flush, flush_mention_queue, expire_mentions, ms_until_a_client_is_ready, M.stop; on_open and broadcast in server/; the handshake, write and send paths in server/client.lua), a check.sh on the shared harness scripts/tla/check-lib.sh, a /statechart view, and headless Neovim replays. check.sh reads 19 guards from the code and checks 8 rules: NoSilentDrop, SentInOrder, AtMostOnce, SentWhenReady, NothingAfterStop, and the liveness rules NoStrandedMention, ReadyDelivers and FlushDelivers. SKILL.md lists the bugs the model found and the PRs that fixed them (#454, #480). Each replay shows the promise kept on the code.
… was described The verify-claudecode-nvim send-queue feature, the queue_timeout comments in config.lua and dev-config.lua, the issue-70 fixture, and the connection_timer type said that mentions wait up to connection_timeout, or named only the connection timeout error. Since #480 a mention times out connection_timeout + connection_wait_delay after its send, a client that is connected but not ready yet gets the Send timeout error, and the deadline timer is nil while a ready client drains the queue.
…f connection_wait_delay The advisor on #480 found that no test pins the length of the wait: a flush that sent once the wait left was under 200 ms passed every test. This one sends 400 ms after the handshake and expects nothing at 550 ms and the mention at 650 ms.
|
@codex review |
|
@codex security review |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. 🚀 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
…ter a spared deadline
…ady client A send 540 ms after the handshake must wait the last 10 ms, so a flush that sent while under 20, 100 or 200 ms of the wait were left fails. A second client that connected later must not hold the flush for the first ready client, so a readiness that waits for the newest client fails.
…ndings, and the scope notes
…mment and the main merge
|
@codex review |
|
@codex security review |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eb16b3aa24
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…rrect the skill's notes churn.lua refreshes the loop clock before the send and measures on it, so a correct deadline no longer reads early. order.lua counts a lost second mention as BROKEN. SKILL.md: churn.lua does not catch a QueueKeepsDeadline revert alone, the cefe258 drain count is 41 to 43, the readiness-wait example keeps the wait > 0 test, --mutants takes up to 50 min, and ClaudeCodeSendComplete fires only on the connected path.
…made while Claude is not connected
…ith a space, and with a relative OUT Codex round 2 on #523: run.sh used GNU timeout, interpolated the checkout path into `set rtp^=`, and built the XDG paths from a relative OUT after the cd. perl's alarm now bounds each run, REPLAY_REPO reaches 'runtimepath' through Lua, and OUT and a relative NVIM become absolute before the loop.
|
@codex review |
|
@codex security review |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
… that a follow-up PR fixes
|
@codex review |
|
@codex security review |
|
Codex Review: Something went wrong. Try again later by commenting “@codex review”. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…, and widen two measured ranges /statechart refuses a view whose state() returns different region keys across states, so a SentWhenReady or NothingAfterStop break charted nothing. The Sent early, Lua error and Sent after a stop regions are now in every state, as "no" until their harm happens. SKILL.md: the cefe258 drain delivered 30 to 45 in our runs, and the immediate path reached Claude a few ms after the handshake (3 to 10 in our runs).
…ine and tcp.lua comment (#539)
|
@codex review |
|
@codex security review |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3ceb6ecc2c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
|
@codex security review |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…sual-edit tests (coder#553) ## What failed CI run 43167645889 (job `integration-tests (stable)`, PR coder#523, head `3ceb6ecc`) failed `tests/integration/selection_visual_edit_spec.lua:230`, "sends no selection for d after a :write in visual mode", with `{false, 'alpha'}`. ## Cause: a timing bug in the test, not in the plugin The test presses `ve`, waits 20 ms, runs `:write`, waits 20 ms and presses `d`. It then asserts that Claude was never sent a non-empty selection. That holds only while `d` comes within the 100 ms debounce of `ve`. When anything delays the keys past it (a slow `:write` on a busy host, a stalled loop), the debounce sends the selection that you are still making, with the mode still visual. That send is right: you did select `alpha`. The plugin then sends the cursor (an empty selection) the moment `d` runs. `sent_no_selection` counted every message, so it also counted the right one. I traced it with time stamps, on Neovim 0.12.3 and 0.8.0. In every failing run the sequence is the same: ``` 0 ms press ve 20 ms write start 167 ms write end 167 ms SENT text="alpha" empty=false mode=v <- the debounce, before d 247 ms press d 297 ms SENT text="" empty=true mode=n <- the cursor, right after d ``` A phantom after `d` would carry ` beta` (the text at the old marks) and a mode other than `v`. None of the 30 traces has one. A deterministic repro makes `:write` block for 150 ms with a `BufWritePost` autocmd: the same sequence, the same failure, and the plugin ends with the cursor held and ` beta gamma` on line 1. The sibling test "sends no selection for d after a plugin edited another line in visual mode" has the same shape and failed too (`wait(20)` stalled). The `ved`, `vec` and `Vj>` tests press all their keys in one `press`, so no timer can fire between them. ## The fix Round 1 (`43a12838`): `sent_no_selection(from)` looks only at the messages after number `from`. The two tests record `#sent` before they press `d`, and assert that a selection sent before the operator is the one the user made. With the flush made to never skip (`if false and entry ...`), the five operator tests fail on both versions, these two included. Round 2 (the Opus verifier's two findings): - **F1, a second flake in the same tests (also on `main`).** After an operator the plugin sends the cursor when its flush or the debounce has run, and `get_latest_selection()` is `nil` until then. A host that stalls the loop past the fixed `wait(400)` made the test fail with `attempt to index a nil value`. The five operator tests (`ved`, `vec`, `Vj>`, and the plugin-edit and `:write` tests) now call `wait_for_cursor()`: `vim.wait` with a condition (the plugin holds an empty selection) and a 5 s limit, then `wait(200)` so that a selection sent late by the debounce would still show. The release tests keep `wait(400)`: the flush on Esc sends the selection before `press` returns, so `get_latest_selection()` is never `nil` there. - **F2, the early-selection check compared only the text.** `assert_is_the_selection` checks the text and the range (`0:0` to `0:5`) of every selection sent before the operator. Round 3 (Codex P2): when the debounce sends `alpha` before `d`, the tests passed even if the plugin only forgot the selection locally and never sent the cursor. Each of the five operator tests now also asserts that the last `selection_changed` sent is a cursor. A mutant whose flush sets `latest_selection` to the cursor without sending it passes the round-2 spec 12 of 12, and fails this spec's plugin-edit and `:write` tests under the 150 ms stall on both versions (a verifier reran it: 2 of 12 fail, those two). Without a stall, `tests/selection_test.lua` already catches that mutant. ## Proof Deterministic stalls, on Neovim 0.12.3 and 0.8.0 (generated copies of the spec, not part of the PR): | Stall | Old spec (`main`) | Round-1 spec | This spec | | --- | --- | --- | --- | | 150 ms between the selection and `d` (`:write` and plugin-edit tests) | 2 of 12 fail (`{false,'alpha'}`) | passes | 12 of 12 pass | | loop blocked 450 ms right after the operator (the five operator tests) | 5 of 12 fail (`attempt to index a nil value`) | 5 of 12 fail | 12 of 12 pass | | no stall | 12 of 12 | 12 of 12 | 12 of 12 | Wrong range, right text (the in-visual selection sent with its end line moved by 5), with the 150 ms stall: the round-1 spec passes 12 of 12 on both versions, and this spec fails the plugin-edit and `:write` tests (both on 0.8.0 and in 3 of 4 runs on 0.12.3, one run failed only the `:write` test). Same-window load (24 runners in parallel and 8 `dd conv=fsync` loops, or 40 runners with 12 disk loops and 16 CPU burners): | Window | Old or round-1 spec: runs that failed | This spec | | --- | --- | --- | | round 1, 0.12.3, old spec | 25 of 72 (`{false,'alpha'}`) | 0 of 72 (round-1 spec) | | round 1, 0.8.0, old spec | 4 of 72 | 0 of 72 (round-1 spec) | | round 2, 0.12.3, 40 runners, round-1 spec | 24 of 80 (all 24 `attempt to index a nil value`: `Vj>` 15, `vec` 7, `:write` 1, `ved` 1) | 0 of 80 (round-2 spec) | | this head (verifier rerun, load up to about 114) | not reproduced: the round-1 spec also passed 0 of 80 | 0 of 80 on 0.12.3, 0 of 40 on 0.8.0 | A window with lower load had no failure for either spec (0 of 72 each), so the deterministic stalls above are the proof and the load windows the check. Other gates: the spec passes 12 of 12 on both versions on an idle host. The CI-style integration runner (private `HOME`, normal and `odd tmp 50%` `TMPDIR`) passes with 417 tests per run on 0.12.3 and 309 per run on v0.8.0, and leaves the real cache untouched. `luacheck` shows 0 warnings in 181 files and `treefmt --ci` 0 changes. ## Notes for the reviewer coder#540 moves `sent_no_selection` to the outer scope of this spec and adds cases that use it. It resolves the conflict when it merges `main`. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-sonnet-5-5` • Thinking: `xhigh`_
…ing off (coder#541) ## What this fixes With `disable_broadcast_debouncing = true` and an empty queue, `_broadcast_at_mention` broadcast a send at once and skipped the readiness check. A Claude whose WebSocket handshake completed less than `connection_wait_delay` ms ago (600 ms by default) got the mention while it was still starting up. On real Neovim 0.8.0 and 0.12.3 it arrived 3 to 10 ms after the handshake. With the default config the same send waits the full 600 ms. The option is not in the README or the `config.lua` defaults. It is typed in `types.lua` and described in `lua/claudecode/README.md`, so a user config can set it. The fix is in `lua/claudecode/init.lua` (`M._broadcast_at_mention`): the immediate path now also needs `ms_until_a_client_is_ready()` to be 0. That means some connected client completed its handshake `connection_wait_delay` ms ago. Otherwise the mention goes through the queue, which sends it once a client is ready. Behavior is unchanged in two cases: - Some client is ready: the send still goes out at once, with nothing queued. - No client reports a handshake time: the immediate path still runs and fails at once with "Failed to broadcast …", as before. The verify-mention-queue skill (coder#523, now on main) found this through a Codex review on its model. coder#523 excluded this config from its `SentWhenReady` rule as a known bug. coder#523 merged first, so this PR adds the model branch and flips that wording. ## The verify-mention-queue model now covers this path - `model/MentionQueue.tla`: a new variable `immediate` holds the user's `disable_broadcast_debouncing`. Each run starts with either value and keeps it. With it on, a send while Claude is connected and the queue is empty broadcasts at once while some connected client is ready. Otherwise the send queues as before. The writes all succeed, all fail later, or are refused at once, as in a flush. - `check.sh`: a new fact, `ImmediateWaitsForReady`, read from `_broadcast_at_mention`. It reads TRUE only when the readiness guard comes before every `.broadcast(` and `queue_mention(` line of that function (a broadcast written as an indexed call or through an alias still reads TRUE; the unit tests catch those), so an unguarded broadcast added before the guard reads FALSE. With it FALSE, `SentWhenReady` breaks. `FailedWriteReported` now also reads the immediate send's `report_unwritten("command", …)`. - `SKILL.md`: `SentWhenReady` holds with `disable_broadcast_debouncing` on or off. The scope note now describes the check instead of a known bug, and the facts table lists the new fact. ## Tests The first two tests were committed before the fix (ebcdb89) and failed on main: - `tests/unit/mention_queue_spec.lua`, `D: with debouncing off, a send right after the handshake still waits for connection_wait_delay`: a fake loop runs a send 100 ms after the handshake. It expects the mention in the queue, nothing at 590 ms and the mention at 610 ms. On main the queue stayed empty and the mention went out at once. - `tests/integration/mention_immediate_send_spec.lua` (real Neovim, so CI runs it on stable and v0.8.0): a loopback Claude completes the handshake. - A send right after the handshake must reach Claude only once `connection_wait_delay` has passed. The time is measured on the loop clock that stamps `connected_at`. On main: `Claude got the mention 3 ms after its handshake, before 600 ms` (0.8.0) and `6 ms` (0.12.3). - A send once Claude is ready must still go out at once, with nothing queued. This one passes before and after the fix. - `tests/unit/mention_queue_spec.lua`, `D: with debouncing off, a send 1 ms before Claude is ready still waits in the queue` (verifier F1 on 24b7ee7, tightened after the verifier on 700cbf6). The first two tests send 500 ms or more before ready, so a readiness check with a tolerance survived them. The fake clock makes the 1 ms exact on any Neovim. Each of these mutations of the guard fails only this test: `< 2`, `< 20`, `<= 20` and `< 100`. With the fix, the spec passes 53 of 53. ## Replay runner and wording (minor findings from coder#523 round 6) - `replays/run.sh`: it exits 2 before the first replay when `NVIM` names no Neovim, and the error names the path as given. A `NVIM` in a missing folder used to turn into `/nvim`. bash's "Alarm clock" note now goes to the replay's log, not the terminal. The comment no longer cites a VimLeavePre error, because Neovim exits 0 after one. - `drain.lua` and `SKILL.md` now say "an 8.6 s wait before Claude connects" where they said "handshake". ## Checks run The gates ran on 4a06553, with main at d123707. The head then merges main's 37ee1e3 (coder#554, coder#564): `diff_inline.lua`, the integration runner and diff specs. None of them touches the mention queue or a file of this PR, and all four `covered.lock` files match the head. Two results ran on earlier commits: - `--mutants` ran on 3980bc3. Since then `check.sh` changed only in how it reads `ImmediateWaitsForReady`, which still reads TRUE, so each mutant run gets the same TLC input. - The statechart runs ran on 334f122. The model is the same on the head. The results on 4a06553: - `verify-mention-queue/check.sh`: PASS, 20 facts TRUE. `main` 540,240 distinct states, `live-NoStrandedMention` 115,628, `live-ReadyDelivers` 115,628, `live-FlushDelivers` 48,296. Three counts are exactly twice the old ones and `live-FlushDelivers` is 1.96 times (24,692 before), because each run checks both values of `immediate`. - `verify-mention-queue/check.sh --mutants`: exit 0 in 6543 s on a loaded host (load average about 60 to 118). Every guard is caught by the rule `EXPECT` names (`ImmediateWaitsForReady=FALSE` by `SentWhenReady`), and every rule breaks at least once. - A scratch copy of the model with an action property that says the immediate send never happens: TLC breaks it (314 states, 1 mention, 1 connect), so the branch is reachable. With `ImmediateWaitsForReady = FALSE`, `SentWhenReady` breaks. - `/statechart` with the committed view still charts three scratch breaks, `StopClearsQueue` and `FlushWaitsForReady` off. All 3 exited 1 with a chart. - Every other `verify-*/check.sh` passes: - `verify-diff-lifecycle`: 89,205 / 5,269 states. Its lock merged from main. - `verify-selection-tracking`: 129,986 states. - `verify-terminal-lifecycle`: 263,060 states. - The selection and terminal models hash all of `init.lua`, so I re-recorded their `covered.lock` files after the fix. Neither model reads `_broadcast_at_mention`. - `replays/run.sh`: 12 of 12 kept on Neovim v0.8.0 and 12 of 12 on v0.12.3, on the head. - With `NVIM=/nonexistent/nvim`, `./no-such-nvim` or `nvim-not-on-path`, it exits 2 before any replay and names the path. - On a copy of the head, a replay that prints KEPT and exits 1 reads BROKEN. A replay that hangs is ended by the alarm and reads BROKEN with exit 142. "Alarm clock" is in its log, and the terminal got 0 bytes of stderr. - busted, full suite, shuffled (seed 12345): 1769 successes, 0 failures, 0 errors, 29 pending. - `scripts/run_integration_tests_individually.sh`: Neovim v0.8.0, 400 successes, 0 failed, 0 errors. Neovim v0.12.3, 537 successes, 0 failed, 0 errors. `mention_immediate_send_spec.lua` passed 2 of 2 on each version. - `ImmediateWaitsForReady` reads TRUE on the head. With an unguarded broadcast before the guard (the verifier's probe R4), it reads FALSE. - luacheck: 0 warnings in 186 files. `treefmt --ci`: 0 files changed. ## Repro ```lua -- In a headless Neovim with the plugin on 'runtimepath', CLAUDE_CONFIG_DIR set to a temp dir: require("claudecode").setup({ terminal = { provider = "none" }, track_selection = false, disable_broadcast_debouncing = true }) -- Connect a WebSocket client, complete the handshake, then right away: require("claudecode").send_at_mention(vim.fn.getcwd() .. "/a.txt", nil, nil, "repro") -- Before this fix the client gets the at_mentioned frame within a few ms. After it, it arrives 600 ms after the handshake. ``` `tests/integration/mention_immediate_send_spec.lua` is the runnable form. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
What this adds
.agents/skills/verify-mention-queue/: a TLA+ model of the @mention queue and the WebSocket clients it sends to, acheck.shon the shared harnessscripts/tla/check-lib.sh, a/statechartview, and 12 headless Neovim replays. CI runs it in thecovered-lockjob with the otherverify-*/check.sh.The model describes the code on main after #454 and #480: one queue, a deadline per mention (
connection_timeout + connection_wait_delayafter the send), a flush that waits until some connected client has been connected forconnection_wait_delaysince its handshake (connected_at), a broadcast to every connected client, the drain to a ready client past the deadlines, and the write reports, the refused write included. It has two client slots, so a broadcast can reach two clients and the readiness rule can count both.This PR also fixes the doc findings from the last verifier pass on #480, and adds the test the #480 advisor found missing:
.agents/skills/verify-claudecode-nvim/features/send-at-mention.md: a connected client that is not ready yet gets "Send timeout", not "Connection timeout", and the error comes at the deadline, 10.6 s after the send.lua/claudecode/config.lua,dev-config.lua: thequeue_timeoutcomments no longer say that mentions wait up toconnection_timeout.fixtures/issue-70/init.lua: the mention waits until its deadline, 10.6 s by default, not 10 s.lua/claudecode/types.lua:connection_timerruns while the queue holds a mention, also while a ready client drains it. It is nil only after a deadline passed while a ready client was connected, untilqueue_mentionor a flush with no ready client starts it again.tests/unit/mention_queue_spec.lua:D: a send 400 ms after the handshake waits until connection_wait_delay has passed since the handshake, and the same at 540 ms. Each expects nothing 590 ms after the handshake and the mention by 610 ms. A flush that sends while under 20, 100 or 200 ms of the wait are left fails one of them.D: the first ready client sends the queue, though a second client connected later is not ready yetfails when readiness waits for the newest client (math.maxin place ofmath.min).These change comments and tests only. No runtime code changes.
What the model checks
NoSilentDrop: a mention you sent reaches Claude, or you see an error, unless you stopped the integration first.SentInOrder: each Claude gets the mentions in the order you sent them.AtMostOnce: each Claude gets each mention at most once.SentWhenReady: a mention goes out only while some connected client has been connected forconnection_wait_delaysince its handshake. Default config only: withdisable_broadcast_debouncingon, a send while the queue is empty skips this wait. That is a known bug, which a follow-up PR fixes (repro below).NothingAfterStop: after:ClaudeCodeStop, no mention queued before the stop reaches Claude, and the queue raises no Lua error.NoStrandedMention(liveness): every queued mention leaves the queue.ReadyDelivers(liveness): while a ready Claude stays connected, every queued mention goes out to it.FlushDelivers(liveness, with no deadline that passes): the flushes alone send a ready Claude every queued mention, so none waits for a deadline first.check.shreads 19 guards frominit.lua,server/init.lua,server/tcp.luaandserver/client.lua, and runs the model with each one as the code has it.FlushExpiresOverduealone breaksNoStrandedMention.QueueKeepsDeadlinealone breaks no rule in this untimed model: the next flush with no ready client clears the overdue mentions, so only the time of the error changes. So--mutantschecks it only as a pair withFlushExpiresOverdue. The unit testsA, F4: a Claude whose handshake comes 10.3 s after the send …and theS:churn tests at 700/300 and 650/500 ms catch it alone.A code change that keeps every fact TRUE reads only STALE, for example a change of the readiness wait or a deadline that never comes.
SKILL.mdnames such changes, and the unit tests catch them.Bugs it found, and the fixes
The first model, on the code before #454, broke five of its six rules. Each finding was replayed on real Neovim. #454 fixed all nine:
queue_timeout, with no error.:ClaudeCodeStopduring the wait raised a Lua error, and a stop and a start sent the old mention to the new Claude.Four more came up in review of #454's fixes and while #480 was built, and this model's second version found one. #480 fixed all five. Items 13 and 14 were in #480's own intermediate commits and never reached main:
drain.luapasses only when all 100 arrive, in order, with no error.on_opencallback late, the deadline found Claude ready before any flush was due, cleared nothing and stopped. When that Claude left beforeon_openran, nothing sent or cleared the queue. The spared deadline now schedules a flush.Proof
On the head of this PR, after merging main at 9bfc39c. The statechart runs ran on 195c1d8. The model and
statechart-view.tsare unchanged since then, and the main merges after it change no mention-queue behavior:check.sh: PASS, exit 0.main270,120 distinct states,live-NoStrandedMention57,814,live-ReadyDelivers57,814,live-FlushDelivers24,692. All 19 facts read TRUE.check.sh --mutants: exit 0 on 86e2359 (the round-3 verifier's run, 2494 s). Every guard is caught by the ruleEXPECTnames, and every rule breaks at least once. Since then the model changed only in comments,check.shandscripts/tla/are byte-identical, and the merged code still reads all 19 facts TRUE, so each mutant run gets the same TLC input.lua/(expire_mentionsspares a ready client's queue without scheduling a flush) makescheck.shexit 1:SparedQueueFlushesreads FALSE andlive-NoStrandedMentionis BROKEN. After the restore,git diff --shortstatis empty.scripts/tla/test-check-lib.sh: 44 cases, 0 failed.replays/run.sh, the same replays on four trees, old trees on Neovim 0.12.3:ordergetssecond.txt, first.txt,stop-in-waitgetsinit.lua:234: attempt to index field 'server' (a nil value), andchurnloses the mention with no error.churnkeeps the mention queued with no error.churn, 10 runs on this head: 10 kept, the mention cleared with one error 10605 to 10655 ms after the send. The replay now refreshes the loop clock before the send and reads the send and the clearing on that clock, the one that stamps the deadline. Before, it read real time against a loop clock that lagged 17 to 26 ms aftersetup(), and printed BROKEN on correct code in about half the runs.orderwith a scratch loss of the second mention (a secondtable.remove(queue, 1)in the flush): BROKEN, exit 1. On this head: KEPT. Before, its verdict counted a lost second mention as kept. The other replays pass their verdict expression straight toL.verdict, so afalseornilreads BROKEN there.replays/run.shportability (Codex round 2): from a checkout path with a space, withOUT=./rel-logsandNVIM=./bin/nvim, 2 of 2 replays kept, and everyXDG_*_HOMEsits under the absoluteOUT. On the same path, eb16b3a'srun.shprinted no verdict (E518: Unknown option). Each run is now bounded byperl -e 'alarm 120; exec @ARGV', not GNUtimeout: the same wrapper withalarm 2ended a hung headless Neovim after 2 s.replays/run.shexit codes (Codex round 5): a replay counts asKEPTonly when it printedKEPTand its Neovim exited 0. I added two scratch replays to a copy of this head. One printsKEPTand then exits 1: it readsBROKEN … Neovim exited 1 after the verdict "KEPT …". The other printsKEPTand quits asL.verdictdoes: it readsKEPT. The 12 real replays readKEPTon Neovim 0.8.0 and on 0.12.3.replays/run.shon Neovim 0.8 (Codex round 3): each replay now runs through:luafile, as.github/workflows/test.ymldoes, sincenvim -lneeds Neovim 0.9. On 0.8.0 and 0.12.3, from a checkout path with a space and with a relativeOUT, a replay that raises before its verdict readsBROKEN … printed no verdictwithin 3 s, with its error in the log.statechart-view.ts:/statechartrefuses a view whose regions change between states, so every state now has the regions "Sent early", "Lua error" and "Sent after a stop". Each one reads "no" until its harm happens. The generator charted three scratch breaks with 1 mention and 1 connect, and each exited 1 with a chart:StopClearsQueueoff, againstNothingAfterStop: the flush after the stop raises a Lua error.FlushWaitsForReadyoff, againstSentWhenReady.StopClearsQueueoff, against~leakedalone: the last state shows "Sent after a stop: a mention queued before :ClaudeCodeStop reached a Claude after the stop".Pele 1.9.0 rendered each chart to SVG. On the same leak input, 93fb4cb's view exited 2 ("Return the same regions for every state") and charted nothing.
verify-diff-lifecycle/check.sh: PASS. Main's fix(server): log Authenticated after the token check and run git once per lookup #539 added a log line toon_openand changed a comment intcp.lua. The flush is still scheduled the same way, so I re-recorded the mention model'scovered.lockaftercheck.shread all 19 facts TRUE.treefmt --ci: 0 files changed. shellcheck (--exclude=SC1091,SC2016) oncheck.shandreplays/run.sh: clean.Repro of the known bug:
disable_broadcast_debouncingskips the readiness waitIt runs the real plugin in a headless Neovim with a loopback Claude, sends right after the handshake, and measures on the loop clock that stamps
connected_at. Save it asrepro-immediate.luain the repo root, then run from there (IMMEDIATE=0is the default config):Output:
How to run
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:xhigh