Repository navigation
Local: a stage stopped by a signal writes its spend line once more - #192
Merged
Merged
Conversation
A stage killed before its next progress-comment write after a pass had spent left that pass at the last line's figure ($0 for a first pass). On SIGTERM, SIGINT or SIGHUP the stage now kills its harness's group, reads the stage's spend at once (stageSpendNow, from each running pass's stream) and, when that is more than the comment shows, PATCHes the comment it last wrote, waiting at most 5 s in a worker thread so no other stage code runs. The session totals that reading counted are recorded as for any interim write, so a resuming stage never counts them twice. Fixes #178 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CS53Gu2D3bE8mKgFN92XP5
…e landed - restSync reads a TokenKeeper getter on this thread (a function cannot go to the worker), and redacts this process's held secrets from the body here (the worker's github.mjs holds none). - One try, and a `sent` field from structure: false when the request never left (the thread or token failed first, github.mjs `unsent` connect codes) or GitHub refused it (4xx); true for a 5xx, no answer in time or a connection lost after sending. The stop write records the session totals only when it landed or may have. - Tests use the getter shape; new cases for a stop write never sent and one answered 502, and restSync's token, redaction and `sent`. - LOCAL.md: Codex reports usage at a turn's end, so a Codex pass killed in its first turn still reads $0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CS53Gu2D3bE8mKgFN92XP5
…cides them Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CS53Gu2D3bE8mKgFN92XP5
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What was wrong: a local stage writes its spend so far only with its progress comment's writes (phase lines, and the status line at most every two minutes). A stage stopped by SIGTERM, SIGINT or SIGHUP (
factory servestopping, Ctrl-C) exited at once fromproc.mjsstopGroups, so a pass that had spent since the last write was left at that write's figure:{"usd":0,"usdLowerBound":true}for a first pass killed seconds in.What changed:
proc.mjsatStop(fn):stopGroupsruns what is registered after it kills the harness groups and before it exits, synchronously, so no other stage code runs (the old guarantee stays).progressReporter(session.mjs) registers one hook after its first write lands. If the stage has no final figure yet, the hook reads the stage's spend at once (harness.mjsstageSpendNow, the same sum asstageSpendSoFarwith each running pass read from its stream throughlive-spend.mjsnow()). If that is more than the comment's line shows, it PATCHes the comment it last wrote (by id, with no list read) with the same body and the new lower-bound line.rest-sync.mjsrestSync: that onerest()call runs in a worker thread while the stage thread waits onAtomics.wait, at mostSTOP_WRITE_MS(5 s). A slow or failed write never holds the exit longer. The loop's own 30 s wait and the stop sequence are unchanged.worker.mjs,review.mjs). restSync reads it on the stage thread and passes the string to the worker as data, never through env or argv.github.mjsholds none, so it would not redact them.sent, decided on structure: false when the request certainly never landed (the thread or token failed before sending, a connect-level failure thatgithub.mjsmarksunsent(ECONNREFUSED, ENOTFOUND, EAI_AGAIN, ENETUNREACH, …), or a 4xx refusal); true when it may have landed (a 5xx, no answer before the deadline, a connection lost after sending).stageSpendSoFarandstageSpendNowshare one sum (stageSpendOf), which keepsstageSpendSoFar's order: finished passes and session totals are taken before any meter is read.docs/LOCAL.md: the spend-line and stopping paragraphs describe the stop write, including Codex's limit.Why it cannot double-count: the stop write records the session totals it counted (
session-totals.json) from the same reading as the figure, as every interim write does. It records them when the write landed or may have, so a resuming stage never counts that part again. If the write may have landed but did not, that part goes uncounted (a lower bound, never a second count). If it certainly did not land, nothing is recorded, and the resuming stage counts the part the line could not.Codex: Codex reports usage only when a turn ends (
turn.completed), so a Codex pass killed in its first turn still reads $0 at the stop. Claude Code (each model response) and Copilot (each usage checkpoint) do not have this limit. LOCAL.md says so; reading Codex's rollout at the stop is not part of this PR.Proof: in
plugin/test/stage-spend.test.mjs, the stage processes hold the token as a getter, as production does. A newtermmode runs the pass in its own process group, as in local mode. The stage writes no line after the pass begins and is sent a real SIGTERM, then exits 143.{"usd": <half a pass>, "usdLowerBound": true}.end, X, end; each run checks the killed stage's line and the sum, with no pass counted twice:term: the killed stage's line holds its half pass, and the sum is 2.5 passes.term-unsent, where GitHub is unreachable for the stop write: nothing is recorded, and the resuming stage counts the half pass, so the sum is 2.5.term-5xx, where the stop write is answered 502: the totals are recorded, and the sum is 2 (the lower-bound case).sentis false for a 422, an unreachable host and a token that fails, and true for a 502 and no answer.Each new test fails without its fix. Without any of this (the test file on
origin/main), bothtermtests show$0. Passing the getter through to the worker breaks every stop write, and the restSync test fails. With no redaction on the stage thread, the restSync test fails. With the earlier commit-on-any-failure rule,term-unsentfails. Withsentalways true,term-unsentand the restSync test fail.Full suite (
node --test plugin/test/*.test.mjs bench/test/*.test.mjs): 784 pass, 0 fail. The temp folder is left empty, andnode bench/skills.mjsis within the cap.Library check: the sync wait is about 30 lines of Node built-ins (
worker_threads,Atomics). The HTTP call goes through Factory's owngithub.mjsrest.synckitdoes the same thing, but it would be a vendored dependency for no protocol or format code.Fixes #178
🤖 Generated with Claude Code
https://claude.ai/code/session_01CS53Gu2D3bE8mKgFN92XP5