Summary
PromptResponse.usage is built from sessionState.lastTokenUsage — the token usage of the final model request of the turn — rather than the session-cumulative sessionState.totalTokenUsage that the adapter already tracks. The result is neither the cumulative-session totals the ACP schema describes nor a per-turn sum: multi-request turns (tool-calling loops) systematically under-report output tokens, and inputTokens reflects the context fill of the last request only.
Where (v1.6.2, tag 9780d31)
buildPromptUsage (src/CodexAcpServer.ts:2750-2755) builds the prompt usage from sessionState.lastTokenUsage; it is attached to every prompt response (end_turn / cancelled / terminal-failure — CodexAcpServer.ts:2387, 2587, 2706, 2723).
CodexEventHandler.ts:1224-1228 stores both lastTokenUsage = toTokenCount(tokenUsage.last) and totalTokenUsage = toTokenCount(tokenUsage.total) from each thread/tokenUsage/updated notification — the cumulative counter is right there.
totalTokenUsage is only ever used to format the /status slash-command text (src/CodexCommands.ts:405); it never reaches any ACP wire surface.
Upstream codex-rs maintains the cumulative sum natively: append_last_usage does total.add_assign(last); last_token_usage = last (protocol/src/protocol.rs:2125-2128, rust-v0.148.0), projected 1:1 into thread/tokenUsage/updated as tokenUsage.total / tokenUsage.last.
Why it matters
The ACP SDK 1.3.0 schema documents Usage fields as session-cumulative: "Total input tokens across all turns", "Sum of all token types across session" (dist/schema/types.gen.d.ts:3050-3085). There is an open debate on whether the session-usage RFD should be clarified to per-turn instead (agentclientprotocol/claude-agent-acp#390) — but last-request semantics matches neither reading: it is not the cumulative session total and not a per-turn sum either, so any client aggregation strategy undercounts.
Concrete downstream impact: intentd (intent-hq/intent) ingests PromptResponse.usage with the schema's cumulative-REPLACE semantics, so codex sessions are undercounted in its token accounting — tracked in intent-hq/intent#3795.
Suggested fix
Send sessionState.totalTokenUsage (already tracked) in PromptResponse.usage. If the RFD lands on per-turn semantics instead, a per-turn sum can be derived by diffing tokenUsage.total snapshots at turn boundaries — either is strictly better than last-request.
Related smaller gaps (same surface, noted while auditing)
toTokenCount (src/TokenCount.ts:27-36) drops cacheWriteInputTokens, so cachedWriteTokens is never sent even though codex-rs reports cache_write_input_tokens.
- codex-rs
fill_to_context_window() (protocol.rs:2130-2144) synthesizes usage with only total_tokens set and all breakdown fields zero; after the adapter's transforms such a report carries all-zero breakdowns, which clients reasonably read as "no usage".
Happy to provide the full audit trail (line-cited) if useful.
Summary
PromptResponse.usageis built fromsessionState.lastTokenUsage— the token usage of the final model request of the turn — rather than the session-cumulativesessionState.totalTokenUsagethat the adapter already tracks. The result is neither the cumulative-session totals the ACP schema describes nor a per-turn sum: multi-request turns (tool-calling loops) systematically under-report output tokens, andinputTokensreflects the context fill of the last request only.Where (v1.6.2, tag
9780d31)buildPromptUsage(src/CodexAcpServer.ts:2750-2755) builds the prompt usage fromsessionState.lastTokenUsage; it is attached to every prompt response (end_turn / cancelled / terminal-failure —CodexAcpServer.ts:2387, 2587, 2706, 2723).CodexEventHandler.ts:1224-1228stores bothlastTokenUsage = toTokenCount(tokenUsage.last)andtotalTokenUsage = toTokenCount(tokenUsage.total)from eachthread/tokenUsage/updatednotification — the cumulative counter is right there.totalTokenUsageis only ever used to format the/statusslash-command text (src/CodexCommands.ts:405); it never reaches any ACP wire surface.Upstream codex-rs maintains the cumulative sum natively:
append_last_usagedoestotal.add_assign(last); last_token_usage = last(protocol/src/protocol.rs:2125-2128, rust-v0.148.0), projected 1:1 intothread/tokenUsage/updatedastokenUsage.total/tokenUsage.last.Why it matters
The ACP SDK 1.3.0 schema documents
Usagefields as session-cumulative: "Total input tokens across all turns", "Sum of all token types across session" (dist/schema/types.gen.d.ts:3050-3085). There is an open debate on whether the session-usage RFD should be clarified to per-turn instead (agentclientprotocol/claude-agent-acp#390) — but last-request semantics matches neither reading: it is not the cumulative session total and not a per-turn sum either, so any client aggregation strategy undercounts.Concrete downstream impact: intentd (intent-hq/intent) ingests
PromptResponse.usagewith the schema's cumulative-REPLACE semantics, so codex sessions are undercounted in its token accounting — tracked in intent-hq/intent#3795.Suggested fix
Send
sessionState.totalTokenUsage(already tracked) inPromptResponse.usage. If the RFD lands on per-turn semantics instead, a per-turn sum can be derived by diffingtokenUsage.totalsnapshots at turn boundaries — either is strictly better than last-request.Related smaller gaps (same surface, noted while auditing)
toTokenCount(src/TokenCount.ts:27-36) dropscacheWriteInputTokens, socachedWriteTokensis never sent even though codex-rs reportscache_write_input_tokens.fill_to_context_window()(protocol.rs:2130-2144) synthesizes usage with onlytotal_tokensset and all breakdown fields zero; after the adapter's transforms such a report carries all-zero breakdowns, which clients reasonably read as "no usage".Happy to provide the full audit trail (line-cited) if useful.