Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9b0bdf8
feat(core): enforce prior read before Edit / WriteFile mutates a file
wenshao Apr 30, 2026
3c78a1f
test(core): clear shared fileReadCache between write-file.test.ts cases
wenshao Apr 30, 2026
3b79cfc
fix(core): close prior-read enforcement gaps flagged in 3rd review
wenshao Apr 30, 2026
303b6b7
refactor(core): close 4 prior-read enforcement gaps from 4th review
wenshao May 1, 2026
d6ffe80
refactor(core): close 4 prior-read enforcement gaps from 4th review (…
wenshao May 2, 2026
248d179
refactor(core): tighten StructuredToolError naming + add scheduler test
wenshao May 3, 2026
323e080
fix(core): close TOCTOU + grammar + directory regressions in PR-B
wenshao May 3, 2026
f4ef756
fix(core): close fileExists TOCTOU on WriteFile prior-read enforcement
wenshao May 3, 2026
a9ec1a8
fix(core): close 4 enforcement gaps + 1 critical bug from 5th Copilot…
wenshao May 3, 2026
fa9344c
fix(core): close new-file race + special-file enforcement loop
wenshao May 3, 2026
9df900a
fix(core): close 4 enforcement gaps from 6th Copilot review
wenshao May 3, 2026
4482dd5
Merge remote-tracking branch 'origin/main' into feat/edit-write-prior…
wenshao May 4, 2026
b053ae3
docs(core): clarify stat→write race + EDIT_REQUIRES_PRIOR_READ scope
wenshao May 4, 2026
40b7a11
Merge remote-tracking branch 'origin/main' into feat/edit-write-prior…
wenshao May 4, 2026
9726c87
chore(core): minor follow-ups from review #4229917446
wenshao May 5, 2026
de8ddf5
fix(core): close 2 correctness gaps from maintainer review #4232751470
wenshao May 6, 2026
72a1d67
fix(core): close 3 correctness gaps from re-review #4233904930
wenshao May 6, 2026
70f7223
docs(core): clarify partial subagent isolation per review #4234090906
wenshao May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions packages/core/src/services/fileReadCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,19 @@ describe('FileReadCache', () => {
expect(entry.lastWriteAt).toBe(Date.now());
});

it('does not set lastReadAt or flip lastReadWasFull', () => {
it('seeds read metadata when recording a write on a brand-new entry', () => {
// The model authored the bytes it just wrote — for the purposes
// of prior-read enforcement on the *next* Edit, that counts as
// having seen the full text content. Without this seeding a
// create→edit→edit chain would reject the second edit because
// lastReadWasFull / lastReadCacheable would still be unset on
// the entry recordWrite created.
const cache = new FileReadCache();
const entry = cache.recordWrite('/x/foo.ts', makeStats());
expect(entry.lastReadAt).toBeUndefined();
expect(entry.lastReadWasFull).toBe(false);
expect(entry.lastReadAt).toBeDefined();
expect(entry.lastReadAt).toBe(entry.lastWriteAt);
expect(entry.lastReadWasFull).toBe(true);
expect(entry.lastReadCacheable).toBe(true);
});

it('refreshes mtime+size so a follow-up Edit sees fresh', () => {
Expand Down
17 changes: 16 additions & 1 deletion packages/core/src/services/fileReadCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,25 @@ export class FileReadCache {
* differ from any prior Read snapshot, so we refresh the cached
* fingerprint to the post-write Stats; otherwise the next Edit would
* see its own write as a "stale" external change.
*
* If the entry has never been Read in this session (i.e. this is a
* fresh file just created by Edit / WriteFile), populate the read
* metadata as well: the model authored the bytes it just wrote, so
* for the purposes of prior-read enforcement it has effectively
* "seen" the full text content. Without this, a create→edit→edit
* chain would be rejected on the second edit because
* `lastReadAt`/`lastReadWasFull`/`lastReadCacheable` would still be
* undefined.
*/
recordWrite(absPath: string, stats: Stats): FileReadEntry {
const entry = this.upsert(absPath, stats);
entry.lastWriteAt = Date.now();
const now = Date.now();
entry.lastWriteAt = now;
if (entry.lastReadAt === undefined) {
Comment thread
wenshao marked this conversation as resolved.
Outdated
entry.lastReadAt = now;
entry.lastReadWasFull = true;
entry.lastReadCacheable = true;
}
return entry;
}

Expand Down
Loading
Loading