feat(opencode): dedup unchanged file reads with a file_unchanged stub - #39997
feat(opencode): dedup unchanged file reads with a file_unchanged stub#39997openchat-ai wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds unchanged-file read deduplication to the read tool so repeated reads of the same text file (same resolved path + range + mtime/size) can return a lightweight “unchanged” stub instead of re-sending file contents, reducing token/cache usage during long sessions.
Changes:
- Introduces
findPriorReadhistory scan and returns an “unchanged” stub when prior non-compacted tool output for the same file+range is still present. - Records
mtimeMsandsizeinreadtool metadata for text-file reads (and in the stub result) to key the dedup check. - Adds a dedicated unit test suite covering the main dedup scenarios and regressions (mtime/size/range/compaction/directory listing).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/opencode/src/tool/read.ts | Adds history-based dedup logic for unchanged text-file reads and records stat metadata used as the dedup key. |
| packages/opencode/test/tool/read.test.ts | Adds unit tests validating stub vs re-read behavior across key stat/range/compaction cases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const offset = params.offset || 1 | ||
| const limit = params.limit ?? DEFAULT_READ_LIMIT | ||
| const mtimeMs = Option.getOrElse(stat.mtime, () => new Date(0)).getTime() | ||
| const size = Number(stat.size) | ||
| if (findPriorRead(ctx.messages, filepath, offset, limit, mtimeMs, size)) { | ||
| return { | ||
| title, | ||
| output: [ | ||
| `<path>${filepath}</path>`, | ||
| `<type>file</type>`, | ||
| "File unchanged since last read. Its full contents are already in the conversation history, so they were not re-sent.", | ||
| ].join("\n"), | ||
| metadata: { | ||
| preview: "File unchanged since last read", | ||
| truncated: false, | ||
| loaded: [] as string[], | ||
| mtimeMs, | ||
| size, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| const loaded = yield* instruction.resolve(ctx.messages, filepath, ctx.messageID) | ||
| const sample = yield* readSample(filepath, Number(stat.size), SAMPLE_BYTES) |
| const input = state.input as { offset?: number; limit?: number } | ||
| if ((input.offset ?? 1) !== offset) continue | ||
| if ((input.limit ?? DEFAULT_READ_LIMIT) !== limit) continue |
findPriorRead returned a file_unchanged stub before Instruction.resolve ran, so newly introduced nearby instruction files (e.g. a new AGENTS.md) were never attached on subsequent reads when the target file itself was unchanged. Resolve instructions before the dedup check so the stub also attaches them. Also align offset normalization with tool execution: findPriorRead used "?? 1" while execute uses "|| 1", so a prior call with offset: 0 (allowed by NonNegativeInt) never matched the dedup.
|
Hi there! First-time contributor here, and I noticed the CI checks on this PR are currently waiting for approval to run (they show as action_required). I'd be grateful if you could approve the workflow run whenever you have a moment — no rush at all. Happy to make any adjustments if needed, thanks! |
Issue for this PR
Closes #39772
Type of change
What does this PR do?
Part of #39772 (cross-session file-read dedup). When the
ead\ tool is called on an ordinary file whose full contents are already in the model's context window and unchanged on disk, this PR returns a \ile_unchanged\ stub result instead of re-reading the file and re-sending its contents. This saves prompt cache_creation tokens and works regardless of model obedience.
Dedup is keyed on the session history (\ctx.messages, exactly what gets sent to the model): a prior read is a match only if it is a completed, non-compacted
ead\ part for the same resolved path with the same offset/limit and the same recorded mtime+size. Because a compacted part has its output cleared, a match guarantees the full contents are still visible to the model, so the stub is safe. Cross-turn and post-resume re-reads are caught automatically since history is reloaded via \ilterCompactedEffect.
The
ead\ result metadata now records \mtimeMs\ and \size\ (no schema change; \metadata\ is an open record). The stub only applies to text-file reads, not directory listings, and never applies within the same assistant turn (concurrent tool calls).
How did you verify your code works?
Added a unit test suite in \ est/tool/read.test.ts\ covering: re-read when never read before, stub when same mtime/size, re-read when mtime changed, re-read when size changed, re-read when a different range was read, re-read when the prior read part was compacted, and re-read when the prior read was a directory listing. All 7 pass. \�un typecheck\ passes.
The pre-existing failures in \ est/tool/read.test.ts\ (\external_directory permission\ git-\ mpdirScoped\ timeouts and a .env\ read timeout) reproduce on clean \origin/dev\ and are unrelated.
Screenshots / recordings
N/A
Checklist