Skip to content

Spend survives a restart: late receipts and abandoned turns are restored; turn.tool carries live spend - #2612

Merged
AbirAbbas merged 6 commits into
devfrom
fix/session-paid-steps-receipts
Nov 13, 2026
Merged

AbirAbbas merged 6 commits into
devfrom
fix/session-paid-steps-receipts

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Three spend changes that were reviewed and merged into workstreams/main but never reached dev. Their integration PR, #2306, is closed: its base app/v3 was retired by #2561, and #2597 left it out. dev still has both faults and lacks the wire field (evidence below), so they are carried here, one commit each.

  • A resumed conversation restores its late receipts (A resumed conversation restores its late receipts, so it never saves less than it charged #2414). A provider receipt that arrives after its stream was cut is banked outside every turn seal and leaves only a journal call line marked with an arm. Replay summed usage seals only, so a restart forgot that money: .25 sealed + .125 late saved .375, a resume read .25, and the next .01 turn saved .26. Replay and Digest.Spend() now count a call line that carries an arm as one call and no turn. Ordinary call lines stay evidence. A paid arm line is also admitted after Close, as usage seals already were.
  • An abandoned turn's paid steps survive a restart (session: an abandoned turn's paid steps survive a restart #2440). A turn let go of at the second stop stage recorded its spend only on its abandoned line, which replay never bills. The displaced loop now writes its own step tally as a usage seal without duration when it leaves its tool batch. A settle turn stopping at its bound and a final stage report write the same seal. The abandoned line stays evidence.
  • turn.tool carries the conversation's live spend (turn.tool carries the conversation's live spend so far #2413). Optional spent_usd, the same meter as agents.budget.get's engine.spent_usd, read after the emitter has released its locks. Zero or unknown readings and history rows omit it. No new event, method or capability. The desktop app already reads this field.

What changed against the originals

  • One conflict: internal/session/digest.go. dev added Digest.folded (folded usage); the late-receipt sum sits beside it and Spend() adds both.
  • Nothing dropped. The change entries are renumbered to this PR.

Evidence that dev still has the faults

With only the original test files placed on dev (80b5861), no code change:

--- FAIL: TestAnAbandonedTurnsStepSpendSurvivesResumeAndTheNextStamp
    abandon_spend_test.go:35: turn exit wrote [], want 1 step seals
--- FAIL: TestOtherTurnEndingsSealTheirStepSpend/settle_bound
    abandon_spend_test.go:368: settle bound seals = [], want one .25 seal with no duration
--- FAIL: TestALateReceiptSurvivesResumeAndTheNextSpendStamp
    spend_replay_test.go:112: resumed meter = {... CostUSD:0.25 ...}, want the seal plus its late receipt
    spend_replay_test.go:117: next turn saved {... SpentUSD:0.26 Tokens:42 ...}, want .385 and 142 tokens after resume
--- FAIL: TestDigestSpendIncludesLateReceiptsWithoutAddingUsageLines/reconciled
    spend_replay_test.go:232: digest spend = 0.25 / 15 tokens, want .375 / 115

Four of the 7 abandoned-turn tests failed (the other three assert behaviour that holds either way), and all 4 late-receipt tests failed. The live-spend tests did not compile: unknown field SpentUSD in struct literal of type TurnTool.

Tests

CI on 3800cf4: every check passes, after one rerun of touched codeaf. Please look at why it needed one:

  • First run: TestFinalPublishDisplacementReachesTheRestoredWindowOnce/engine/displaced=true failed with final publish did not encounter displacement, in its shard and again on the classifier's solo retry. The base probe skipped the test (no engine binary in the base worktree), so attribution was inconclusive. The rerun of the job passed. Locally the subtest passed 25 of 25. It exercises sync's final publish at quit, which this PR does not touch, and it is not yet listed in touched packages: flaky or already-failing tests #1749. A later repeat on macOS, with the engine built in both trees so nothing skips, passed 300 of 300 on this branch (3800cf4) and 300 of 300 on unmodified dev (80b5861), at 1, 2 and 4 CPUs. So the CI failure has not been reproduced on either tree and is still not attributed.
  • TestProcessCloseJoinsTheUsageWriter and TestDoReturnsAfterCancelledAccountingAndItsWriterStop were classified flaky, as on many unrelated runs in touched packages: flaky or already-failing tests #1749.

Local, macOS, focused runs with the fix: pass.

  • The 4 late-receipt, 7 abandoned-turn and 4 live-spend tests.
  • go test -run 'Abandon|SettleBound|StageBatch|StageReport|LateRe|LateCall|Reconcil|Digest|RestoredUsage|CallLines|SpendStamp|Seal' ./internal/session/: ok, 98 top-level tests.
  • ./internal/cellindex/ and ./internal/appwire/: ok.

Local make pr-ready did not pass on this Mac. The PR is marked ready on the strength of CI and the comparison with dev below. The light gate and touched rest passed. The heavy legs are red here for reasons that are also red on unmodified dev (80b5861) on the same machine:

  • touched session: 5 failures (TestACommandLookFailsOnAFailingPipeStage, TestWatchTickFailsOnAFailingPipeStage, TestBashBeltTruncationCapFollowsTheWindow, TestBashBeltTruncationWritesHeadTailAndPath, TestTheFixedPrefixStaysUnderItsBudget). All 5 fail on dev here too.
  • touched codeaf: 14 failures while other suites were running on the box. 9 are Factory and memory-worker tests that fail on dev here too, alone. The other 5 pass when run alone on this branch.
  • The first light-gate attempt had one failure, TestATrackedConversationIsReachedAndAForgottenOneIsNot (TempDir RemoveAll cleanup: ... directory not empty); it passed 3 of 3 alone and on the second attempt.

Changes after review

Two review findings, both fixed in commits on top of 3800cf4. The CI run described under Tests was on 3800cf4, before these commits.

  1. The manual's transcript-format section contradicted the code (6354eb8).

    • Found: in internal/manual/chat/sessions-and-rewind.md, the section "What is saved in a conversation transcript" still said a call line is never added up. Since this PR a resume does add a call line marked with an arm (a late receipt), once, as the page's own usage and call-line sections already say.
    • Changed: the paragraph now says a call line is normally not added up, because its money is already in the usage line that closed its turn, and that a late receipt marked with an arm is the exception: its money is in no usage line, so a resume adds it once.
    • Proving test: TestTheManualNamesTheArmWhereverItSaysCallLinesAreNotAddedUp (internal/session/spend_replay_test.go) reads the page and requires every paragraph that says call lines are not added up to name the arm exception. It failed on the old paragraph and passes now. The behaviour itself was already covered by TestLateCallLinesRestoreSpendWithoutAddingTurns and TestDigestSpendIncludesLateReceiptsWithoutAddingUsageLines.
  2. The three new step seals did not save the conversation's total to meta.json (eca765e).

    • Found: the abandoned tool batch, the settle-bound stop and the final stage report wrote their step seal to the journal but never stamped meta.json. Their steps bank as turn calls, which Agent.bank leaves for the seal to stamp, and these exits do not go through sealTurn. So meta.json kept the figure from before the steps. After a restart with no further turn, the meter (/cost, the status row, agents.budget.get) restored the journal's total, while spent_usd in agents.list/agents.open/agent.changed and the Home card kept the older figure, because stampRestoredSpend only fills in a missing figure. Example: a $0.01 title, then a $0.25 step ending at one of these exits, read $0.26 on the meter and $0.01 in meta.json. The same gap existed before any restart, until the next ordinary seal; a Factory stage chat that ends on its stage report may never seal again.
    • Changed: the three exits now share a new Agent.sealSteps, which writes the step seal without duration as before and then calls the existing deferred stampSpend. That stamp writes only SpentUSD/Tokens from the live books and does not touch LastReplyAt or anything a replacement turn owns. The manual's usage-lines paragraph and the change entry (docs/changes/unreleased/2612-abandoned-turn-step-seal.md, whose surface now includes app) say so.
    • Proving test: TestAStepSealSavesTheTotalAResumeRestores (internal/session/abandon_spend_test.go), subtests abandoned, settle_bound and final_report. Each checks meta.json against the live meter before a restart, then compares the resumed meter, SavedSpend (what agents.list reads) and the journal's Digest.Spend(). All three failed before ($0.26 on the meter, $0.01 in meta.json) and pass now, also with -count=8 and with -race -count=3 alongside the other abandoned-turn tests.

Evaluated and left: for the second finding the reviewer also suggested letting stampRestoredSpend raise meta.json on resume. Not done: it would fix only the after-restart symptom and leave the gap between the step seal and the next ordinary seal while the engine is still running.

🤖 Generated with Claude Code

AbirAbbas and others added 4 commits November 12, 2026 11:55
Carries #2413 from the closed integration PR #2306 (workstreams/main);
it never reached dev.

Co-Authored-By: Claude Mythos 5.2 <[email protected]>
…less than it charged

Carries #2414 from the closed integration PR #2306 (workstreams/main);
it never reached dev. The one conflict, Digest's fields, keeps dev's
folded-usage total beside the late-receipt sum.

Co-Authored-By: Claude Mythos 5.2 <[email protected]>
Carries #2440 from the closed integration PR #2306 (workstreams/main);
it never reached dev.

Co-Authored-By: Claude Mythos 5.2 <[email protected]>
AbirAbbas and others added 2 commits November 12, 2026 18:21
The transcript-format section of sessions-and-rewind.md still said every
`call` line is never added up. Since this pull request a resume adds a
call line marked with an `arm` once, as the usage and call-line sections
of the same page already say. The section now names that exception, and
a test keeps every paragraph that says call lines are not added up
naming the arm beside it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…2612)

The abandoned tool-batch, settle-bound and final stage-report exits wrote
their step seal to the journal but never stamped meta.json. Their steps
bank as turn calls, which Agent.bank leaves for the seal to stamp, so
meta.json kept the figure from before them. After a restart the meter
(/cost, the status row, agents.budget.get) restored the journal's total
while meta.json (agents.list spent_usd, Home) kept the older one, because
stampRestoredSpend fills only a missing figure. A .01 title followed by a
.25 step at any of the three exits read $0.26 on the meter and $0.01 in
meta.json until the next ordinary turn sealed.

The three exits now share sealSteps, which writes the step seal without
duration and owes the deferred spend stamp. The stamp reads the live
books and writes only the spend fields, so it changes nothing a
replacement turn owns. The manual and the change entry say so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@AbirAbbas
AbirAbbas merged commit 8efd86e into dev Nov 13, 2026
9 checks passed
@AbirAbbas
AbirAbbas deleted the fix/session-paid-steps-receipts branch November 13, 2026 00:37
AbirAbbas added a commit that referenced this pull request Nov 13, 2026
Brings in #2613, #2612, #2614, #2618 and #2615. #2618 changes the
visibility check this branch's job-events cursor reads: a spent GitHub
rate limit returns GitHub's retry time instead of a cached denial, and is
remembered per token until then.

The two meet in repoAllowed, which this branch made answer (allow, known).
A remembered limit answers false, false: a limit says nothing about the
repository, so the job-events page ends before it as before any undecided
one, as a fresh limit already does (askRepo's cache=false).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant