Skip to content

fix(always-on): a resumed transcript pairs turn-1 attachments with a later turn - #30

Merged
pa-arth merged 2 commits into
mainfrom
fix/resumed-prefix-turn
Aug 15, 2026
Merged

fix(always-on): a resumed transcript pairs turn-1 attachments with a later turn#30
pa-arth merged 2 commits into
mainfrom
fix/resumed-prefix-turn

Conversation

@pa-arth

@pa-arth pa-arth commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Review findings from #28 and #29, both real.

1. Resumed prefix used the wrong turn (#28, Greptile)

injectedOpen closes at the first main-chain assistant row — 34 lines BEFORE the
cross-file seen dedup — while computeAlwaysOn measures the prefix from
firstMain, the first RETAINED turn. On a resumed transcript those are different
rows: the closing row is a replay owned by an earlier file and gets dropped, so
firstMain lands on a later turn whose cacheRead has the whole replayed
conversation folded in. Turn-1-sized attributed against a much larger measured
INFLATES fixedPrefixTokens, and reconcile() cannot catch it because it only
fires on a NEGATIVE remainder. Inflation is the silent direction.

Closing before the dedup was deliberate and stays — it is what stops a whole
resumed session's attachments folding into "turn 1". The bug was the other half.

Fixed by recording which row closed the window and which row firstMain will
pick, and declining to measure the session when they differ — counted, not
silently dropped. standingContextTokens is untouched: a resumed turn really did
carry that much. Only the BREAKDOWN is unanswerable, and unmeasured now says so
with its own reason rather than borrowing the no-attachments one.

ZERO INSTANCES IN THE AUTHORING CORPUS: 0 of 483 sessions with attachments, of
926.
Verified by same-instant runs of origin/main and this branch — 0 of 30
fields moved. So this fixes no number today; it closes a path that would have been
invisible when it did fire.

That also means the case cannot be recorded, and a suite running only real local
data would stay green with the guard deleted. It is built from RECORDED bytes
anyway: a resumed replay is "rows this parse has seen, then rows it has not", so
seeding seen from a prefix of a recorded transcript and re-parsing the whole of
it reproduces the shape exactly, with no invented rows.

Two recorder bugs surfaced doing that, both the fixture-encodes-an-impossible-shape
failure the recorder exists to prevent:

  • message.id was filled with CONSTANT filler, so every assistant row shared an id
    and the parser merged them — a "two-turn" recording came back as one turn with one
    prefix. Identity keys are now a memoized bijection: distinctness and intentional
    sharing both survive, no real value is carried. The old comment claimed ids "key
    nothing across fixtures"; they key the dedup and the streamed-row merge.
  • the recorder counted assistant ROWS, not turns. Now counts distinct message ids.

2. Corrected rates had no offline coverage (#29, Greptile)

pricingDrift degrades to pass when it cannot fetch — correct, and it means an
offline run proves nothing about the rows it just corrected. pricingPinned.test.ts
is the deterministic companion: drift asks whether the VENDOR moved, this asks
whether OUR table moved underneath us. Only rows with a reason to be pinned are in
it; pinning all 29 would train people to update the expectation to match the code.

3. A stale comment that would have caused the next mis-pricing

Writing that test failed it, on gpt-5.5-pro. The header claimed "the pro tiers
publish no cached-input rate" and are set to cachedInput == input. False since
gpt-5.4-pro/5.5-pro
: both publish 3/M, the ordinary 10%, confirmed against
LiteLLM, and the table correctly carries it. A reader trusting that comment would
have "restored" them to equal-input and over-billed every cached token they read.
The real exception set is three rows and is now named with reasons rather than
derived from the word "pro" — gpt-5-pro and gpt-5.2-pro (genuinely no published
rate) and codex-mini-latest (genuinely 25%).

410/410 tests, typecheck and lint clean. Mutation-checked: reverting luna to the
launch tier, "fixing" gpt-5.5-pro to equal-input, and dropping the prefix-turn gate
each fail only their own test. The two-turn fixture asserts its own precondition
(turn 2's prefix 61,014 -> 61,863) so a future re-recording cannot quietly leave the
guard proving nothing.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

🤖 Generated with Claude Code

…later turn

Review findings from #28 and #29, both real.

### 1. Resumed prefix used the wrong turn (#28, Greptile)

`injectedOpen` closes at the first main-chain assistant row — 34 lines BEFORE the
cross-file `seen` dedup — while `computeAlwaysOn` measures the prefix from
`firstMain`, the first RETAINED turn. On a resumed transcript those are different
rows: the closing row is a replay owned by an earlier file and gets dropped, so
`firstMain` lands on a later turn whose cacheRead has the whole replayed
conversation folded in. Turn-1-sized `attributed` against a much larger `measured`
INFLATES `fixedPrefixTokens`, and `reconcile()` cannot catch it because it only
fires on a NEGATIVE remainder. Inflation is the silent direction.

Closing before the dedup was deliberate and stays — it is what stops a whole
resumed session's attachments folding into "turn 1". The bug was the other half.

Fixed by recording which row closed the window and which row `firstMain` will
pick, and declining to measure the session when they differ — counted, not
silently dropped. `standingContextTokens` is untouched: a resumed turn really did
carry that much. Only the BREAKDOWN is unanswerable, and `unmeasured` now says so
with its own reason rather than borrowing the no-attachments one.

**ZERO INSTANCES IN THE AUTHORING CORPUS: 0 of 483 sessions with attachments, of
926.** Verified by same-instant runs of origin/main and this branch — 0 of 30
fields moved. So this fixes no number today; it closes a path that would have been
invisible when it did fire.

That also means the case cannot be recorded, and a suite running only real local
data would stay green with the guard deleted. It is built from RECORDED bytes
anyway: a resumed replay is "rows this parse has seen, then rows it has not", so
seeding `seen` from a prefix of a recorded transcript and re-parsing the whole of
it reproduces the shape exactly, with no invented rows.

Two recorder bugs surfaced doing that, both the fixture-encodes-an-impossible-shape
failure the recorder exists to prevent:
- `message.id` was filled with CONSTANT filler, so every assistant row shared an id
  and the parser merged them — a "two-turn" recording came back as one turn with one
  prefix. Identity keys are now a memoized bijection: distinctness and intentional
  sharing both survive, no real value is carried. The old comment claimed ids "key
  nothing across fixtures"; they key the dedup and the streamed-row merge.
- the recorder counted assistant ROWS, not turns. Now counts distinct message ids.

### 2. Corrected rates had no offline coverage (#29, Greptile)

`pricingDrift` degrades to pass when it cannot fetch — correct, and it means an
offline run proves nothing about the rows it just corrected. `pricingPinned.test.ts`
is the deterministic companion: drift asks whether the VENDOR moved, this asks
whether OUR table moved underneath us. Only rows with a reason to be pinned are in
it; pinning all 29 would train people to update the expectation to match the code.

### 3. A stale comment that would have caused the next mis-pricing

Writing that test failed it, on `gpt-5.5-pro`. The header claimed "the `pro` tiers
publish no cached-input rate" and are set to cachedInput == input. **False since
gpt-5.4-pro/5.5-pro**: both publish 3/M, the ordinary 10%, confirmed against
LiteLLM, and the table correctly carries it. A reader trusting that comment would
have "restored" them to equal-input and over-billed every cached token they read.
The real exception set is three rows and is now named with reasons rather than
derived from the word "pro" — gpt-5-pro and gpt-5.2-pro (genuinely no published
rate) and codex-mini-latest (genuinely 25%).

410/410 tests, typecheck and lint clean. Mutation-checked: reverting luna to the
launch tier, "fixing" gpt-5.5-pro to equal-input, and dropping the prefix-turn gate
each fail only their own test. The two-turn fixture asserts its own precondition
(turn 2's prefix 61,014 -> 61,863) so a future re-recording cannot quietly leave the
guard proving nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Aug 15, 2026

Copy link
Copy Markdown

Greptile Summary

The PR prevents resumed Claude Code transcripts from pairing first-turn attachments with a later retained turn, adds deterministic pricing pins, and improves recorded multi-turn fixture identity handling.

  • Tracks the row closing prefix collection and excludes resumed mismatches from always-on component medians.
  • Adds a recorded two-turn resumed-transcript fixture and regression coverage.
  • Adds offline assertions for selected OpenAI prices and corrects cached-input pricing documentation.
  • Extends the fixture recorder to preserve distinct IDs and record multiple assistant turns.

Confidence Score: 4/5

The PR appears safe to merge, with a non-blocking fixture-recorder fidelity issue around tool-result identity references.

The runtime resumed-transcript gate and pricing coverage are coherent, but newly recorded fixtures break the equality between tool-call IDs and tool-result back-references, weakening tests that consume tool result timing or error metadata.

Files Needing Attention: scripts/record-injected-fixture.mjs

Important Files Changed

Filename Overview
src/adapters/claudeCode.ts Tracks whether collected attachments and the first retained main-chain usage belong to the same turn.
src/alwaysOn.ts Excludes resumed prefix mismatches from component medians and reports a dedicated unmeasured reason.
src/injectedPrefix.ts Extends injected-prefix metadata with the turn-alignment flag and a conservative default.
scripts/record-injected-fixture.mjs Adds multi-turn recording and synthetic identity preservation, but omits the parser-relevant tool_use_id back-reference.
src/tests/injectedPrefix.test.ts Reproduces resumed dedup from recorded bytes and verifies that mismatched prefix breakdowns are excluded.
src/tests/pricingPinned.test.ts Adds deterministic offline checks for historically regressed pricing rows and cached-input exceptions.
src/vendor/pricing.ts Corrects documentation of GPT-5 cached-input pricing exceptions without changing runtime rates.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Parse assistant row] --> B[Record prefix-closing turn key]
  B --> C{Row retained after dedup?}
  C -- No --> D[Continue to later rows]
  C -- Yes --> E[Record first retained main-turn key]
  E --> F{Keys match?}
  F -- Yes --> G[Measure injected-prefix breakdown]
  F -- No --> H[Exclude breakdown and report resumed reason]
  G --> I[Keep standing-context measurement]
  H --> I
Loading

Fix all with Greploop

Fix All in Claude Code Fix All in Cursor

Reviews (1): Last reviewed commit: "fix(always-on): a resumed transcript pai..." | Re-trigger Greptile

Comment thread scripts/record-injected-fixture.mjs Outdated
Greptile P2 on #30, valid — and the failure was one step worse than reported.
`tool_result.tool_use_id` names a `tool_use.id`, which `claudeCode.ts:270` reads to
fold is_error and the result timestamp back onto the issuing turn. `id` went
through the identity bijection; `tool_use_id` fell through to length-preserving
filler. Because it is LENGTH-preserving, the two real ids were the same length and
both results came out as the same string — so the fixture did not merely lose the
link, it pointed two distinct calls at one key. A tool_result naming a tool_use
that does not exist is a transcript Claude Code cannot emit, which is the exact
class of defect recording fixtures exists to prevent (and the second instance of it
in this PR).

Added to IDENTITY and the two-turn fixture re-recorded from the same transcript: 4
lines change, the recorded prefixes are byte-identical (61,014 -> 61,863), so the
resumed-transcript assertions rest on the same numbers as before.

Also asserted, across EVERY injected-prefix fixture rather than the one that
regressed: every tool_use_id resolves to a tool_use in the same file, and distinct
results name distinct calls. The second half is not redundant — containment alone
passes the moment filler collides with a real id, which is how this hid.

411/411, typecheck and lint clean. Checked the guard bites: the pre-fix fixture
fails it by name ("dangling tool_use_id redacted-fixture-content redac").

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pa-arth

pa-arth commented Aug 15, 2026

Copy link
Copy Markdown
Owner Author

Correction to the last commit message: the suite is 414/414, not 411 — the new join check is an it.each over the four injected-prefix-* fixtures, so it adds 4 tests, not 1. The commit body has the wrong count; amending it needs a force-push, which this machine's presence hook blocks while another session holds a claim, so it is corrected here and in the squash message rather than routed around.

@pa-arth
pa-arth merged commit 03b9245 into main Aug 15, 2026
2 checks passed
@pa-arth pa-arth mentioned this pull request Aug 15, 2026
pa-arth added a commit that referenced this pull request Aug 15, 2026
Four merged PRs since 0.7.0 (#27 #28 #29 #30), and two of them change numbers a
user has already read, which is what makes this a release rather than a merge.

MINOR, not patch: #28 changes what the always-on breakdown MEANS — measured off the
transcript instead of censused off disk — and takes the aggregate schemaVersion 9 ->
10, removing `skillDescriptionTokens` rather than keeping a field whose name outlived
its meaning.

THE ONE TO SHIP FOR: 0.7.0 prices gpt-5.6-luna 5x over and terra 1.25x over on every
Codex session it has audited since the vendor repriced (backend f4500a27, 2026-07-31,
two days after 0.7.0 went out). That was fixed on main on 2026-08-14 and reaches
nobody until this publishes — a merged pricing fix in an unreleased CLI is a fix that
does not exist for users.

Verified before tagging, not just built:
- 414/414 tests, typecheck and lint clean
- `npm run build:npm` -> 411.9kb single file, `--version` reports 0.8.0 (so the
  esbuild --define baked the bump, which is the thing a version bump can silently
  miss)
- ran the BUNDLE against the real local corpus, not just `--help`: schemaVersion 10,
  standingContextTokens 60,963 — within 0.4% of the 60,738 measured independently by
  differencing `claude -p` runs — `skillDescriptionTokens` absent from the payload as
  #28 intended, `unmeasured` empty.

Tag v0.8.0 after merge; .github/workflows/publish.yml guards tag == package.json and
publishes + attaches the standalone binaries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pa-arth
pa-arth deleted the fix/resumed-prefix-turn branch August 27, 2026 23:44
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