You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The CLI now passes a signal into runToolLoad (clients/cli/src/handlers/load-interrupt.ts). On the first SIGINT/SIGTERM, no new calls start and in-flight calls are allowed to finish. The summary then prints with "stoppedEarly": { "reason": "interrupted", "detail": "received SIGINT" }, and the CLI exits 130 with envelope code interrupted (new EXIT_CODES.INTERRUPTED). Thresholds are not checked on an interrupted run. A second signal exits 130 immediately.
CLI --load flag refusals (including --load-connections above --load-concurrency, and above 1 over stdio) now use the same load_flag_conflict code as mcpdo. The exit code stays 1. The code is a shared constant, LOAD_FLAG_CONFLICT.
The engine stops starting calls once 50 calls in a row have thrown, or once calls have done nothing but throw for 1 s (at least 5 of them). Only thrown calls count. A tool error or a success resets the streak. The summary reports stoppedEarly: { reason: "consecutive_errors", detail }, and a warning goes to stderr in both CLIs. The exit code does not change.
mcpdo's existing abort on a terminal connection status now reports reason: "connection_lost".
These fields appear only with --load-rate: plannedStarts, droppedSlots and achievedRate. They are computed over the window the run spent starting calls: the full duration, up to the last start for a request-bound run, or up to the stop.
When achievedRate is below 90% of the target rate, a warning goes to stderr. --quiet drops it in the CLI.
A run that reaches its --load-duration measures elapsedMs to at least the end of that window, so a --load-rate 1 run over 5 s now reports 1/s.
--load-max-dropped is not added.
Behavior change to note
latencyMs and --load-max-p99 used to include failed calls. They no longer do. This is the point of #2836, and it is documented in clients/cli/README.md, clients/mcpdo/README.md and skills/mcpdo/SKILL.md.
Tests
New clients/cli/__tests__/load-summary.test.ts covers:
engine tests for each fix, using a fake clock now shared via helpers/load-clock.ts
the interrupt helper, including a second signal and listener cleanup
in-process CLI runs: a Ctrl+C run that exits 130 with a partial summary; load_flag_conflict codes; isError text tally; p99 failing with no successes; the rate-shortfall warning with and without --quiet; an HTTP server stopped mid-run that trips the breaker
mcpdo: human-format rendering of the new fields, the connection_lost abort reason, and an end-to-end rate-shortfall warning.
… split success latency, report dropped rate slots (#2834, #2835, #2836, #2837)
- CLI: SIGINT/SIGTERM abort the run instead of the process; the partial
summary prints with stoppedEarly.reason "interrupted", then exit 130.
A second signal exits at once. Load flag refusals use load_flag_conflict.
- Engine: circuit breaker after 50 consecutive thrown calls (or 1s of only
thrown calls), reported as stoppedEarly consecutive_errors; mcpdo names a
dropped connection connection_lost.
- latencyMs is success-only, errorLatencyMs times failures, okThroughput
added, --load-max-p99 judges successes; isError text is tallied in
toolErrorMessages.
- --load-rate runs report plannedStarts/droppedSlots/achievedRate and warn
on stderr under 90% of target; a duration-bound run is measured to the
end of its window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
…p, breaker only while starts remain, uncapped planned slots, exact shortfall check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Rate accounting can falsely report dropped requests and shortfall warnings when timers wake slightly late or an interrupt occurs after the request or duration boundary.
End rate window at boundary or stop time during interruption
core/cli/handlers/load-test.ts:671
An interrupt while in-flight calls finish extends the rate-accounting window past the bound that already ended starts. With requests: 1 and rate: 1, interrupting at 5 s while the only call is still running reports five planned starts and four dropped slots; interruption after the duration deadline similarly inflates the counts. Preserve the interrupted reason and exit 130, but end the rate window at the earliest applicable request boundary, duration boundary, or stop time.
Copilot round 2: both findings are fixed in 2cfa44e.
Inline: a request-bound window now ends at the scheduler's nextStart, so a slightly late final timer no longer counts as a dropped slot. I replied in the thread.
Previously missed (no thread to reply into): an interrupt that lands while calls drain past a bound no longer stretches the rate window. The window now ends at the earliest of three points: the duration deadline, the request bound's next slot, or the stop time. The run is still reported as interrupted and still exits 130. New tests cover both the request-bound case and the deadline case.
After a CLI upgrade, ensureDaemon() can reuse an older running daemon whose load summary lacks errorLatencyMs and toolErrorMessages. This branch then throws on e.min, losing the human-readable summary; toolErrorMessages.length below has the same problem. Handle the older payload consistently in rendering and threshold checks, or detect an incompatible daemon and report an actionable restart requirement before running the load test.
Correct rate metrics when claimed slots are not invoked
core/cli/handlers/load-test.ts:617
claimSlot() has already incremented started, so a worker that returns here after another worker aborts the run still counts as a started call without invoking call. The new rate figures therefore overstate achievedRate and undercount dropped slots in interrupted or breaker-stopped summaries. Undo the claimed start before returning, or track actual invocations separately.
Copilot round 3 had no inline comments. It raised two findings in its "previously missed" block, which have no threads, so I'm answering them here.
A claimed slot that was never used still counted as started. Fixed in 794b072. A worker that claims a slot after the run has stopped now hands it back, so achievedRate and droppedSlots count only the calls that actually ran. The new test ("does not count a slot claimed after the run stopped as started") fails without the fix.
Copilot round 4 had no inline comments and no suppressed block, so I'm closing the review loop.
Its headline sentence repeats two points:
Older daemon summaries. Already declined in round 3 as pre-existing version skew between the mcpdo front end and its daemon.
Teardown-time interrupts. A signal that arrives after the run has ended, while the summary is being written or the client is disconnecting, falls to Node's default SIGINT/SIGTERM handling. That is deliberate (see withLoadInterrupt's header): the process dies by the signal, which the shell reports as 130 / 143. This PR does not change that path.
…ts as the second Ctrl+C (#2834)
when-exit (via atomically) re-raises SIGINT/SIGTERM with process.kill after
its cleanups, so the CLI's listener saw two signals for one press and
exited 130 before the partial summary was written. A repeat of the same
signal within 500ms of the first is now treated as that re-raise. Adds a
spawn test that delivers real signals to the built CLI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
#2834 follow-up, fixed in f5c2de5. The rollup smoke found that one real SIGINT or SIGTERM to the built CLI exited 130 with an empty stdout.
Cause.when-exit@2.1.5 arrives via atomically and listens for these signals too. After running its cleanups it unconditionally re-raises the signal with process.kill(process.pid, signal). Our listener received that copy as a "second Ctrl+C" and exited before the drained summary was written. The in-process tests call the listener directly, so they could never see the re-raise.
Fix. A repeat of the same signal within REPEAT_GRACE_MS (500 ms) of the first is now treated as a re-raise and ignored. A press after that, or a different signal at any time, still exits 130 at once.
Why a time window. A listener cannot tell where a signal came from: the re-raise is a real kernel signal. Removing or reordering a dependency's listeners would mean reaching into their internals, and when-exit re-raises whatever we do. The re-raise lands within milliseconds. A person pressing again is reacting to the "Press Ctrl+C again" line, so a press inside the window costs at most one extra press.
Test. New clients/cli/__tests__/load-interrupt-e2e.test.ts spawns build/index.js and delivers real signals:
one SIGINT, and one SIGTERM: each prints the partial summary and exits 130
a second SIGINT 700 ms later while calls hang: exits 130 at once with no summary
With the window disabled, the two single-signal cases fail, so the test reproduces the bug. The unit tests now drive the window with an injected clock. local:gate passes.
Copilot round 5, on f5c2de5: no inline comments, no suppressed block, and no open issues. The headline only repeats the legacy-daemon payload point, which I declined in round 3 as pre-existing front-end/daemon version skew. Closing the review loop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2834
Closes #2835
Closes #2836
Closes #2837
All four issues live in the shared
--loadengine (core/cli/handlers/load-test.ts), so one PR covers them. The CLI and mcpdo get the same fixes.#2834: Ctrl+C / SIGTERM keeps the partial summary
signalintorunToolLoad(clients/cli/src/handlers/load-interrupt.ts). On the first SIGINT/SIGTERM, no new calls start and in-flight calls are allowed to finish. The summary then prints with"stoppedEarly": { "reason": "interrupted", "detail": "received SIGINT" }, and the CLI exits 130 with envelope codeinterrupted(newEXIT_CODES.INTERRUPTED). Thresholds are not checked on an interrupted run. A second signal exits 130 immediately.--loadflag refusals (including--load-connectionsabove--load-concurrency, and above 1 over stdio) now use the sameload_flag_conflictcode as mcpdo. The exit code stays 1. The code is a shared constant,LOAD_FLAG_CONFLICT.#2835: circuit breaker for a dead server
stoppedEarly: { reason: "consecutive_errors", detail }, and a warning goes to stderr in both CLIs. The exit code does not change.reason: "connection_lost".#2836: success and failure latency
latencyMsnow coversokcalls only. The newerrorLatencyMstimes failures, and the newokThroughputis the success-only throughput.--load-max-p99therefore judges successful calls only, and it fails when none succeeded ("no call succeeded…").isErrorresult's first text block, trimmed and capped at 200 characters, is tallied into the newtoolErrorMessages.#2837: dropped rate slots
--load-rate:plannedStarts,droppedSlotsandachievedRate. They are computed over the window the run spent starting calls: the full duration, up to the last start for a request-bound run, or up to the stop.achievedRateis below 90% of the target rate, a warning goes to stderr.--quietdrops it in the CLI.--load-durationmeasureselapsedMsto at least the end of that window, so a--load-rate 1run over 5 s now reports 1/s.--load-max-droppedis not added.Behavior change to note
latencyMsand--load-max-p99used to include failed calls. They no longer do. This is the point of #2836, and it is documented inclients/cli/README.md,clients/mcpdo/README.mdandskills/mcpdo/SKILL.md.Tests
clients/cli/__tests__/load-summary.test.tscovers:helpers/load-clock.tsload_flag_conflictcodes;isErrortext tally; p99 failing with no successes; the rate-shortfall warning with and without--quiet; an HTTP server stopped mid-run that trips the breakerconnection_lostabort reason, and an end-to-end rate-shortfall warning.npm run local:gatepasses.🤖 Generated with Claude Code