Skip to content

fix(acp): persist token usage events for ACP sessions - #1137

Merged
kevincodex1 merged 2 commits into
Twigpine:mainfrom
FabioLeitao:fix/1118-acp-usage-events
Oct 15, 2026
Merged

kevincodex1 merged 2 commits into
Twigpine:mainfrom
FabioLeitao:fix/1118-acp-usage-events

Conversation

@FabioLeitao

@FabioLeitao FabioLeitao commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

ACP sessions dropped every provider token count, so zero usage report showed zero requests and zero tokens for editor-driven work. zero exec and the TUI already persist those counts as session usage events. This wires the same writer into the ACP turn.

The full suite, including Windows and macOS, already passed on the fork CI (FabioLeitao#12, merged).

This pull request was prepared with AI assistance. The change was reviewed against the current runTurn path, including the session permission mode from #1134, and checked locally: the regression tests fail when OnUsage is removed, and an isolated local OpenAI-compatible stub showed zero usage on the upstream binary and the stub's token counts after the change.

Problem

internal/acp/agent.go runTurn set OnText, OnReasoning, OnToolCall, OnToolResult, and OnPermissionRequest, and left agent.Options.OnUsage unset. The agent loop still received provider usage. Nothing appended a sessions.EventUsage record. zero usage report (including --session) totals only those records, so an ACP session that had user messages, assistant messages, and tool events still reported zero tokens. Reproduced on main at 99721c76 and on v0.9.0. #929 (usage on the ACP wire) is a separate change and stays out of this PR.

Cause

The exec runtime records usage with usage.EventUsagePayload from OnUsage. The ACP turn runner never installed that callback, so the counts never reached the session store.

Fix

runTurn queues each report through usage.EventUsagePayload into the turn's existing buffered batch and commits it with persistTurn under the session id created by Store.Create. That is the id zero usage report --session already filters on.

The payload matches a non-escalating zero exec run: promptTokens, completionTokens, totalTokens, plus cachedInputTokens, cacheWriteTokens, and reasoningTokens only when non-zero. There is no model key, because ACP has no mid-run model escalation. The payload has no prompt text, tool arguments, or other raw input.

A hard RunAgent failure still returns before persistTurn, so that turn's usage is discarded with its history. StopCancelled remains a successful outcome, and usage the provider already reported stays in the log. session/load and session/resume still project only messages, compaction summaries, and tool events, so the restored prompt and the wire replay stay the conversation.

The permission mode persisted on the session (#1134) is unchanged. The ACP wire protocol, exec, the TUI, and the report reader are unchanged.

Before / after

Before: a finished ACP turn left messages and tool events and no usage events. zero usage report --session <id> --json showed "requests": 0 and zero tokens.

After: the same turn leaves one usage event per provider report. Against a local OpenAI-compatible stub that emitted 1200 prompt tokens and 34 completion tokens, with an isolated home directory:

Binary requests input output total
upstream main 0 0 0 0
this branch 1 1200 34 1234

Both turns ended end_turn. totalCost stayed 0 because the stub model id is not in the price registry. The token counts are what the report starts to see.

Tests

  • TestACPPromptPersistsProviderUsage: two provider reports in one turn become two usage events in the report's shape, beside the turn's two messages. Payload keys are token counts only.
  • TestACPHardTurnFailureDropsItsUsageWithTheTurn: a hard RunAgent error leaves no durable events.
  • TestACPCancelledTurnKeepsReportedUsage: context.Canceled commits the reported usage with stopReason cancelled.
  • TestACPLoadAndResumeIgnoreUsageEvents: session/load replays the user and assistant text only, and the resumed prompt contains the conversation and no token counts.

Removing the OnUsage wiring makes TestACPPromptPersistsProviderUsage fail with agent.Options.OnUsage is not wired and usage events = []. The cancel and load/resume tests fail the same way.

Risk

Usage joins the turn batch, so a crash mid-write has the same boundary it already had for messages. A cancelled turn that already received a provider usage report now counts those tokens; a hard failure still does not. Cost for an unknown model stays a zero estimate, as it does for any other session whose model id is absent from the registry. OnUsage runs on the agent loop goroutine, the same place OnToolCall already appends to the turn buffer.

How to verify

gofmt -l on the two files, go vet ./internal/acp/..., go test ./internal/acp ./internal/usage/... -race -count=1, go run ./cmd/zero-release build, and go run ./cmd/zero-release smoke.

End to end, with an isolated home directory and a local stub provider: zero acp (session/new, session/prompt), then zero usage report --session <id> --json. The fixed binary counts the stub's tokens. The upstream binary reports zeros.

Scope

internal/acp/agent.go and internal/acp/usage_test.go only. No UI change.

Linked issue

Closes #1118

The parent issue has the issue-approved label.

Checklist

  • The linked issue already has the issue-approved label.
  • go build ./..., go vet ./..., and go test ./... pass locally.
  • gofmt clean.
  • Tests added/updated for the change (and run under -race where relevant).
  • UI changes include screenshots or a short recording where possible.

Local commands run on this branch: gofmt -l on both touched files (clean), go vet ./internal/acp/..., go test ./internal/acp ./internal/usage/... -race -count=1, go run ./cmd/zero-release build, and go run ./cmd/zero-release smoke. The full repository suite, including Windows and macOS smoke, passed on the fork CI for the same tests (FabioLeitao#12). There is no UI change, so there is no screenshot.

Summary by CodeRabbit

  • New Features
    • Provider token-usage reports are now recorded alongside session activity, including cached input token counts.
    • Usage records remain available when a turn is cancelled successfully.
  • Bug Fixes
    • Usage from failed turns is no longer retained.
    • Recorded usage does not appear in conversation replay or get included in subsequent prompts.

ACP turns never set agent.Options.OnUsage, so provider token counts were
dropped on the floor. zero exec and the TUI persist the same counts as
session usage events, and `zero usage report` totals those events, which
left every editor-driven ACP session at zero requests and zero tokens.

runTurn now queues each report through usage.EventUsagePayload, the same
writer exec uses for a non-escalating run, and commits it in the turn's
existing buffered batch under the session id. A hard run failure still
returns before persistTurn, so that turn's usage is dropped with its
history. StopCancelled stays a successful outcome and keeps the usage
already reported. session/load and session/resume still skip event types
other than messages, compaction, and tools, so the restored prompt and
the wire replay do not gain a usage channel. ACP has no model escalation,
so the payload has no "model" key and no prompt, tool argument, or other
raw input.

This sits beside the permission-mode field persisted on the session
(Twigpine#1134). It does not change the ACP wire protocol, exec, the TUI, or the
report reader.

Refs Twigpine#1118
The persistence test showed that a successful turn records token counts,
and the hard-failure test showed that an aborted turn records nothing.
Neither one locked the two edges that are easy to get backwards.

A client cancel is a StopCancelled outcome, not a hard failure, so the
tokens the provider already reported stay in the session log. session/load
and session/resume read that same log; the replay and the next prompt
must still be the conversation, with no usage payload on the wire and no
token counts folded into the resumed prompt. The persisted JSON is also
checked to be token-count keys only, so a prompt or tool argument cannot
ride along in the usage record.

Refs Twigpine#1118
@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: Twigpine/zero/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d5df0b88-b543-4fb4-845e-8b7456f7faf3
📥 Commits

Reviewing files that changed from the base of the PR and between 846d30f and 947dfab.

📒 Files selected for processing: 2
  • internal/acp/agent.go
  • internal/acp/usage_test.go

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

Walkthrough

ACP now persists provider token-usage reports as session events in the turn’s buffered batch. Tests cover persistence, turn failures, cancellation, and session load and resume.

Changes

ACP usage persistence

Layer / File(s) Summary
Queue and persist usage with the turn
internal/acp/agent.go, internal/acp/usage_test.go
ACP queues provider usage using the shared usage payload and persists it with the turn. Tests cover token counts, hard-failure discard, cancellation retention, and exclusion of usage events from replayed and resumed conversation context.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: vasanthdev2004, gnanam1990, euxaristia

Merge Risk: ⚪ Minimal · up to 947df

ACP usage is compatible with usage reports, including session-filtered reports. No merge-blocking issue is established.

Security architecture risk: 🔵 Low · up to 947df

Editor-driven sessions now retain token counts without changing permissions or conversation replay. No new security exposure was identified in the reviewed paths, but broader security coverage remains incomplete.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • observed — The added data flow carries provider-reported token counts into the active session's existing event log and usage report. Persistence obtains the session association from sess.id rather than from the usage report.

Trust Boundaries and Controls

  • observed — The callback only queues accounting metadata. Session model and workspace selection, permission mode, permission requests, and turn ownership continue through the existing session-controlled execution path; the base-to-head change does not modify those controls.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: persisting token usage events for ACP sessions.
Linked Issues check Passed The PR meets the coding requirements in [#1118]. internal/acp/agent.go wires agent.Options.OnUsage through usage.EventUsagePayload into the existing buffered turn batch. The batch persists `sess…
Out of Scope Changes check Passed The changes stay within [#1118]. The source change is limited to ACP usage persistence in internal/acp/agent.go. The added tests in internal/acp/usage_test.go directly verify the issue behavior an…
  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
🧪 Generate unit tests (beta)
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@FabioLeitao

Copy link
Copy Markdown
Contributor Author

CI note: the only red job is Smoke (windows-latest), in internal/sessions: TestForkAndChildHoldTheParentTheyAreCreatedFrom/fork (prune_test.go:415, parent kept for "", want "open in another Zero process").

This PR only touches internal/acp. internal/sessions is identical to main at 846d30f5, where Smoke (windows-latest) passed (run), so this looks like an intermittent failure in that test and not this change. I filed it as #1138 with the log.

Could a maintainer re-run the failed job? The other nine checks pass. I am not pushing an empty commit to re-trigger it.

@kevincodex1
kevincodex1 merged commit 300cf62 into Twigpine:main Oct 15, 2026
17 of 18 checks passed
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.

acp: ACP sessions never persist token usage, so zero usage report does not count them

2 participants