Skip to content

fix(claude-code): stop re-sending file reads in full after they were trimmed - #92

Merged
trancefornow merged 2 commits into
zjunlp:mainfrom
boxabirds:claude/upstream-3-claude-code-repeat-read
Sep 30, 2026
Merged

trancefornow merged 2 commits into
zjunlp:mainfrom
boxabirds:claude/upstream-3-claude-code-repeat-read

Conversation

@boxabirds

Copy link
Copy Markdown

Summary

TokenPilot shortens large file reads before Claude Code sends them to the model. Because of a bug, the next request sent the same file in full again. That undid the saving and broke the prompt cache from that point on. This PR keeps a file shortened for as long as that same read is in the conversation.

In a local probe through the gateway's reduction path, a Read result trimmed to 2,830 characters went out at its full 36,580 characters on the very next request.

What was going wrong

tool_payload_trim has a deliberate rule: if the model reads a file it has already seen in shortened form, it probably wants the whole file this time, so that second read is left in full.

To apply the rule, the gateway saves every path it has shortened (disclosedReadPaths) and passes the list back in on the next request. But Claude Code resends the entire conversation on every request, so the first read is sent again too. The pass then mistook that same old read for a new re-read and sent it in full.

The fix

The session snapshot now also records which read first disclosed each path, keyed by its tool_use_id (disclosedReadOwners). A path is carried into the next request only when that read is no longer in the conversation.

Situation Before After
The same read is still in the conversation Sent in full (bug) Stays shortened, byte-identical
That read has left the conversation (e.g. compaction) and the model reads the file again Sent in full Sent in full (unchanged)
Snapshot from an older version (paths but no owners) Paths carried Ignored; the read is shortened as normal

The last row is a one-off cost after upgrading: a deliberate re-read in an existing session may come back shortened once.

Files changed:

  • src/reduction.ts: owner tracking, and the carry rule above.
  • src/session-state.ts and src/gateway-runtime.ts: persist the new field alongside disclosedReadPaths.
  • src/context-cleaner/session-catalog.ts: validates the new field.

Tests

New file tests/reduction-disclosed-reads.test.ts:

  • O1–O3: unit tests for the three helpers.
  • D1: the same read stays shortened and byte-identical on the next request. This is the regression test.
  • D2: a genuine re-read after the original has left the conversation is sent in full.
  • D3: an old-format snapshot does not force reads to go in full.
  • D4: the new field survives saving.
  • C1: the cleaner's session list validates the new field.

In tests/gateway-runtime.test.ts, a new test sends two requests through the real gateway with the same read in both. The existing test for genuine re-reads passes unchanged.

Checks run:

  • Mutation check: D1, D3 and the new gateway test fail when the old behaviour is restored.
  • pnpm build, pnpm typecheck and check:boundaries pass.
  • claude-code passes 260/260, and the other CI suites pass.

Prepared with help from Claude Code; the commit is marked Co-Authored-By: Claude.

claude and others added 2 commits September 29, 2026 13:01
…requests

The tool_payload_trim pass sends a file read in full when its path was already
disclosed, taking it as the model asking for the whole file again. The Claude
Code gateway persisted every disclosed path and passed them all back in on the
next request. Claude Code resends the whole history, so the pass saw the same
read as a repeat of itself and sent it untrimmed: a probe showed a Read result
trimmed to 2,830 chars on one request going out at 36,580 chars on the next,
which also breaks the prompt cache from that point on.

The session snapshot now records which tool_use_id disclosed each path
(disclosedReadOwners). A path is carried into the next request only when that
read is no longer in the history, so a genuine re-read after compaction still
comes back in full. Snapshots from before this change (paths without owners)
are ignored rather than carried.

Tests: tests/reduction-disclosed-reads.test.ts (O1-O3, D1-D4, C1) and a gateway
test for the same read staying trimmed. D1, D3 and the gateway test fail
against the previous behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6it2Si8k8XNGMF8RdyMQz
@trancefornow
trancefornow merged commit 3c5fbc6 into zjunlp:main Sep 30, 2026
2 checks passed
boxabirds pushed a commit to boxabirds/LightRSI that referenced this pull request Sep 30, 2026
Applies the refinement made to the Claude Code fix in zjunlp#92 (eb1eb2c) to Codex:

- a path's owner must be a `read`/`file_read` result that tool_payload_trim
  actually trimmed in this request, not merely the first tool result with that
  path (a write to the same file, or an untrimmed read, can no longer own it);
- owners are rebuilt from the pass's bounded reported path set instead of
  accumulating.

Tests O4, O5 and D5 mirror the Claude Code ones; O4 and D5 fail without the
new ownership filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6it2Si8k8XNGMF8RdyMQz
boxabirds pushed a commit to boxabirds/LightRSI that referenced this pull request Oct 1, 2026
Adds an in-process OpenCode v1 server plugin (`components/adapters/opencode`)
plus the host glue it uses (`components/adapters/shared/canonical`), built on the
shared packages and mirroring the Claude Code adapter. Refs zjunlp#93.

- Stabilizer: `experimental.chat.system.transform` (developer target; the user
  target is not possible in 1.18.33 because messages are transformed first).
- Reduction: request-time in `experimental.chat.messages.transform`, through
  the shared `@lightrsi/reduction` pipeline over a canonical envelope.
- Recovery: the shared recovery MCP server, registered in `opencode.json`.
- Eviction (opt-in, needs taskStateEstimator settings): the shared
  canonical-surface cycle, applied as a durable per-session overlay.
- Repeat-read rule: as in zjunlp#92, a disclosed path is carried into the next
  request only when the trimmed read that disclosed it has left the history;
  the record is saved per session so restarts keep it.
- Reversible install with backups (jsonc configs are printed, never
  rewritten); never writes credentials; every hook fails open and logs to
  `<stateDir>/tokenpilot/adapter.log`.

Tests: 189 in the package. The shared glue (config, reduction, memo store,
before-call, eviction, logger) has one test file per module, each listing the
cases it covers; plus runtime, codec/overlay, install/doctor and registration
tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6it2Si8k8XNGMF8RdyMQz
Xubqpanda pushed a commit that referenced this pull request Oct 3, 2026
Adds an in-process extension for the pi coding agent
(`@earendil-works/pi-coding-agent`, verified on 0.87.1) in
`components/adapters/pi`, built on the shared host glue from the OpenCode
adapter (`components/adapters/shared/canonical`). Refs #97.

- Stabilizer: `before_agent_start` system-prompt sections, so pi appends a
  section delta instead of replacing the prompt.
- Reduction: request-time in the `context` hook, through the shared
  `@lightrsi/reduction` pipeline over a canonical envelope.
- Recovery: a native `memory_fault_recover` tool with the shared MCP tool's
  name, description and schema (pi has no MCP).
- Eviction (opt-in, needs taskStateEstimator settings): the shared
  canonical-surface cycle, applied as native `context_edit` entries at
  `turn_end`, before pi's own threshold compaction.
- Repeat-read rule: the shared disclosed-read memo, as in #92 and the
  OpenCode adapter, saved per session so `pi -p` / `--continue` restarts keep
  it.
- Reversible install with backups; never writes credentials; every hook fails
  open and logs to `<stateDir>/tokenpilot/adapter.log`.

Tests: 86 in the package (runtime, codec, install/doctor, registration). The
shared glue is already covered by the OpenCode package's suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6it2Si8k8XNGMF8RdyMQz
boxabirds pushed a commit to boxabirds/LightRSI that referenced this pull request Oct 3, 2026
Applies the refinement made to the Claude Code fix in zjunlp#92 (eb1eb2c) to Codex:

- a path's owner must be a `read`/`file_read` result that tool_payload_trim
  actually trimmed in this request, not merely the first tool result with that
  path (a write to the same file, or an untrimmed read, can no longer own it);
- owners are rebuilt from the pass's bounded reported path set instead of
  accumulating.

Tests O4, O5 and D5 mirror the Claude Code ones; O4 and D5 fail without the
new ownership filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6it2Si8k8XNGMF8RdyMQz
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.

3 participants