Skip to content

Agent spend is complete: agents.open and agent.changed carry the saved figure, and late receipts save it - #2347

Merged
AbirAbbas merged 6 commits into
app/v3from
fix/agent-spend-complete
Nov 4, 2026
Merged

AbirAbbas merged 6 commits into
app/v3from
fix/agent-spend-complete

Conversation

@AbirAbbas

Copy link
Copy Markdown
Collaborator

Summary

Two generic gaps in an agent's saved spend (Agent.spent_usd, the cumulative figure in the conversation's meta.json):

  1. agents.open and agent.changed never carried it. Only agents.list (listedView) set SpentUSD; the cached
    view behind open answers and every agent.changed had none, so a client that takes those over its list row lost
    the figure.
  2. A charge recovered after a cancelled run never reached it. (*Agent).reconciled (Cancelled calls keep the money that was measured #2302) banked the late
    receipt into the meter and the machine ledger, but nothing stamped meta.json and no per-agent change was sent, so
    agents.list under-reported until something else rewrote the file.

Change

  • internal/session: reconciled stamps the cumulative meter after banking (the same snapshot door a turn's seal
    uses; no ledger or call-line sums on top). Spend stamps hold one mutex from capture to write, so an older captured
    total cannot be written after a newer one. Every successful save is told to a new process-wide hook,
    OnSpendSaved(SpendSaved{Dir, SpentUSD}), in the style of OnUsageWritten.
  • cmd/codeaf: saved spend is read from meta.json in the same step that says an agent (beside unread, under
    unreadSay), for every agent.changed through the inbox tap and for the open answer; there is no app-side spend
    cache, so a view copied earlier can never deliver an older figure. A save restates the agent once, off the stamping
    goroutine (coalesced per conversation), only when the figure differs from what was last said. Other method answers
    keep their existing path and omit the figure. agents.budget.get keeps the live meter.
  • Docs: PROTOCOL "Saved agent spend", the chat manual section, and a change entry.

Known and unchanged: after a resume the restored meter sums journal usage lines and not late call lines, so a later
turn's stamp can save a figure lower than a late charge saved earlier (pre-existing replay gap; not repaired here).

Validation contract

Item Tests
(a) agents.open reports the same spend as agents.list for the same agent cmd/codeaf/appspendsaved_test.go: TestAgentsOpenAndListAgreeOnSavedSpend (cold and held, both folder layouts), plus the stale-snapshot and stale-row regressions
(b) after a late-recovered cancellation charge, agents.list reports the higher spend and one agent.changed carries it internal/session/spendsaved_test.go: TestLateRecoveredCancellationSavesCumulativeSpendOnce, TestAnsweredTurnPublishesItsSavedSpend; cmd/codeaf/appspendsaved_test.go: news sends exactly one change, duplicates none; a stalled emitter does not hold the stamp
(c) no charge counted twice TestLateRecoveredCancellationSavesCumulativeSpendOnce (X+L saved, one ledger row for L); TestConcurrentTurnAndLateSpendStampsKeepTheLatestMeter (an older capture cannot overwrite a newer total)

Reviewed by fresh independent review passes; findings from earlier rounds (stale views, a blocking hook, an untested
capture race, method answers waiting on a stalled event writer) are fixed and covered above.

🤖 Generated with Claude Code

AbirAbbas and others added 6 commits November 4, 2026 09:40
…ay so

A receipt recovered after a cancelled turn ended moved the meter and the
machine ledger but never meta.json, so every reader of the saved total
under-reported until another turn sealed. reconciled now stamps the
cumulative meter after banking. Spend stamps hold one mutex from capture
to write, so an older captured total cannot land after a newer one, and
each successful save is told to OnSpendSaved with the conversation's
folder and the saved figure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….list reports

Only agents.list set Agent.spent_usd. The open answer and every
agent.changed now carry the figure saved in meta.json, read in the same
step that says the agent (beside unread), so a view copied earlier cannot
deliver an older figure. A spend save restates the agent once, off the
stamping goroutine, when the figure differs from what was last said.
Other method answers keep their path and omit the figure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eceipts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts.open

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guard law requires every critical section in the guarded tree to
unlock from a defer directly under its Lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AbirAbbas

Copy link
Copy Markdown
Collaborator Author

CI on the final head 527ed278c (PR gate does not trigger for app/v3, so runs were dispatched on the branch):

  • Full check runs 41855737715 (shard 1 rerun) and 41865992865: cross builds, remote and shard 1 green (shard 1 includes the guard lock law this branch first tripped and now passes).
  • Remaining reds are the same as a fresh app/v3 at 0f8574efc (Full check 41850823339, PR gate 41850828817):
    internal/session TestALandStartsNoAutomaticMaintenance, TestNoRoadInTheMemoryEngineHasMoreEndingsThanTheCeiling,
    TestNoRoadInTheTaskEngineHasMoreEndingsThanItsLedgerRow, TestProgramHoldClassifiesEveryRegisteredChatTool,
    TestTheFixedPrefixStaysUnderItsBudget, TestTheLeanPrefixStaysUnderItsBudget; and cmd/codeaf hitting the 15-minute
    package timeout.
  • One-off timing failures in untouched packages, each green on the other run of the same code:
    internal/lane TestS3ItComesBack, internal/remote TestTaskWorkspaceEarlierCallerDeadlineStopsResidentOperation,
    internal/session TestTeamTrafficNoTeamChatsShareOneWatch (a process-wide goroutine count).
  • The new session tests passed in every run. The new cmd/codeaf tests ran before the package timeout in Full check
    41850781156 (the run reached files after appspendsaved_test.go), and none failed.

🤖 Generated with Claude Code

@AbirAbbas
AbirAbbas merged commit 1f1a733 into app/v3 Nov 4, 2026
34 of 48 checks passed
@AbirAbbas
AbirAbbas deleted the fix/agent-spend-complete branch November 4, 2026 16:52
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