Skip to content

lifecycle: keep a no-change turn's tokens for every agent - #2722

Merged
peyton-alt merged 6 commits into
mainfrom
peyton/stop-hook-tokens-no-changes
Oct 15, 2026
Merged

peyton-alt merged 6 commits into
mainfrom
peyton/stop-hook-tokens-no-changes

Conversation

@peyton-alt

@peyton-alt peyton-alt commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

https://entire.io/gh/entireio/cli/trails/1536

Bug. When a turn changes no files, turn-end skips SaveStep. That path recorded only out-of-band usage (Antigravity), which caused two losses:

  • Cursor: the stop payload's event.TokenUsage was dropped. Cursor's transcript has no usage fields, so the turn was missing from both the session total and the next checkpoint.
  • Claude Code, Codex and other agents that keep usage in their transcripts: the turn never reached the session total (entire status). Their checkpoints were unaffected, because condensation recomputes them from the transcript slice.

A Factory AI Droid Worker turn that changed no files also returned before the Worker routing, so it left no task record on the parent, and its transcript and tokens never reached the parent checkpoint. This one predates the PR.

Fix.

  • resolveTurnTokenUsage is now the single resolver for both turn-end paths. Precedence: hook-reported, then Codex's subagent ledger, then the transcript from the turn's start offset, then out-of-band.
  • SaveStep's accounting is extracted as applyTurnTokenUsage. It covers the session total, the pending checkpoint usage, the subagent baseline rescoping, and dropping a child snapshot computed against a stale Codex ledger version. SaveStep and ApplyTurnTokenUsage both use it.
  • A no-change turn's tokens are applied inside the same MutateSessionState that moves the session from ACTIVE to IDLE, and only while it is ACTIVE. A repeated, retried or concurrent Stop for the same turn finds it IDLE and adds nothing. Without this, a retried Claude Code Stop, which runs after the pre-prompt state is cleaned up, re-added every earlier turn.
  • Droid Workers are resolved before the no-changes gate. A read-only Worker turn becomes a completed, zero-file task record on the parent, which is itself a condensation trigger. Each Worker turn adds its own usage, from the turn's start offset, to that record. Only the Stop that ends the Worker's own active turn adds it. Condensation drops a completed record once it is stored, so a Worker turn already in one checkpoint is never counted by the next, and none of it lands on the Worker's own session total.
  • A Stop that finds no active turn records nothing. That covers a repeated Stop, and also a turn whose TurnStart never fired, which has no start offset to scope the usage to. Every built-in agent fires a TurnStart; Droid installs its prompt hook on SessionStart too, so exec mode is covered.

No checkpoint double-counts the turn. For transcript agents, condensation still takes the main counters from the transcript slice, and the pending checkpoint usage is only a fallback.

Not in this PR. SaveStep (the file-changing path) has the same repeated-Stop exposure, which predates this PR: a retried Stop adds the tokens again and also records an extra step. The phase gate does not carry over, because SaveStep also legitimately runs on a non-ACTIVE session, for example the first Stop after Entire is enabled mid-session, which initializes state as IDLE. Fixing it needs a per-turn step marker, so it is a follow-up.

Tests.

  • Integration tests for Cursor, Claude Code and Codex drive the real hooks: a file-changing turn, a no-change turn, a commit, then one more turn and commit. They assert each turn lands once in the session total and once in a checkpoint. All three fail on main.
  • Repeated and concurrent Stop tests for Cursor (hook tokens) and Claude Code (transcript tokens) on no-change turns assert that the session total and the pending checkpoint usage hold the turn exactly once. Without the phase gate they read 200 for 100 (Cursor) and 280 for 140 (Claude Code).
  • A Droid integration test covers a read-only Worker that consumes tokens, with its Stop fired twice, then a commit, a second Worker turn, and a second commit. It asserts the parent's completed task record, the Worker transcript and task.json usage in the first checkpoint, that the Worker's own session records none, and that the second checkpoint's task holds only the second turn. It fails before this change.
  • Strategy unit tests cover subagent rescoping and a stale ledger version on the no-change path.

🤖 Generated with Claude Code

A turn-end that changes no files skips SaveStep, and that branch only
recorded out-of-band usage (Antigravity). The hook-reported usage on
the event (Cursor's stop payload) was dropped. Cursor's transcript has
no usage fields, so condensation could not recover it either: the turn
was lost from the session total and from the next checkpoint.

Record event.TokenUsage on that branch, falling back to out-of-band
usage only when the hook reported none, the same precedence the
SaveStep path uses, so no turn is counted twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M4HPW4188RCPJJK89HX2XPNM
@peyton-alt
peyton-alt requested a review from a team as a code owner October 10, 2026 01:32
Copilot AI balanced review requested due to automatic review settings October 10, 2026 01:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The fix matches existing token-source precedence and is covered by focused end-to-end regression testing.

0 open findings

What changed in this PR

Fixes lost Cursor token usage when a turn changes no files by preserving hook-reported usage in session and checkpoint accounting.

Changes:

  • Uses hook-reported usage before out-of-band fallback on checkpoint-less turns.
  • Adds integration coverage for session totals and checkpoint scoping across three turns.
File Description
cmd/​entire/​cli/​lifecycle.go Preserves token usage when SaveStep is skipped.
cmd/​entire/​cli/​integration_test/​cursor_token_condensation_test.go Verifies no-change turn accounting and checkpoint isolation.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

…files

The checkpoint-less turn-end path recorded only hook-reported and
out-of-band usage, so for Claude Code, Codex and the other agents
whose usage lives in the transcript, a turn that changed no files
never reached the session total that `entire status` shows. (The next
checkpoint was unaffected: condensation recomputes it from the
transcript slice.)

Resolve the turn's usage on that path exactly as SaveStep does, with
one helper (resolveTurnTokenUsage: hook, Codex child ledger,
transcript from the turn's start offset, out-of-band), and record it
through the same accounting SaveStep uses, now extracted as
applyTurnTokenUsage: the session total, the pending checkpoint
window, the subagent baseline rescoping, and dropping a child
snapshot computed against a stale Codex ledger version. Subagent
sessions of their own (Droid Workers) are skipped, as SaveStep never
runs for them either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@peyton-alt peyton-alt changed the title lifecycle: keep hook-reported tokens for turns that change no files lifecycle: keep a no-change turn's tokens for every agent Oct 10, 2026
peyton-alt and others added 4 commits October 13, 2026 12:41
…Workers

A no-change turn's tokens were accumulated before the turn-end transition,
with no check that the turn was still open, so a repeated, retried, or
concurrent Stop added them again. A retried Claude Code Stop was worse: with
the pre-prompt state already cleaned up it resolved its transcript offset from
the session and re-added every earlier turn. The tokens are now applied inside
the MutateSessionState that performs the ACTIVE->IDLE transition, and only
while the session is ACTIVE, so the second Stop finds it IDLE and adds nothing.

A Factory AI Droid Worker turn that changed no files returned through the
no-changes gate before the Worker routing, so it left no task record on the
parent and its transcript and tokens never reached the parent checkpoint.
Workers are now resolved before that gate, and their record carries the
Worker's usage computed from its whole transcript (recounted at
condensation), so it lands on the parent's task once and never on the
Worker's own session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M4XQRQ3HNAPCABZXM4XG28D6
A multi-turn Droid Worker's record carried whole-transcript usage that
condensation recounted from line 0, and condensation drops a completed
record once stored, so a turn already in one checkpoint was counted again
by the next. Each Worker turn now adds only its own slice (from the turn's
start offset) to the record, and only when ending the Worker's own turn
claims it, so a repeated Stop adds nothing; the record is no longer marked
for a whole-transcript recount.

Usage counted as applied only after the turn-end mutation saved, so a
failed save is reported. The log for a Stop with no active turn no longer
claims it was a duplicate: with no turn start there is no offset to scope
the usage to, so it is not recorded in either case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M4Y2P7H5M6JSVCYPKHR2MXPS
@peyton-alt
peyton-alt merged commit 8b9dff6 into main Oct 15, 2026
19 checks passed
@peyton-alt
peyton-alt deleted the peyton/stop-hook-tokens-no-changes branch October 15, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants