Repository navigation
fix(web): file saves no longer overwrite newer disk changes - #1869
Conversation
Each save names the disk revision its draft started from; the server refuses a stale write and the editor asks to overwrite or reload. Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes
🧩 Effect Service Conventions
|
| Head | Result | Time | Check |
|---|---|---|---|
798ac75 |
✅ clear | 2 min | run |
789763f |
✅ clear | 2 min | run |
8f154fa |
✅ clear | 4 min | run |
048b82b |
✅ clear | 4 min | run |
6a1bafe |
✅ clear | 3 min | run |
189da4a |
✅ clear | 1 min | run |
bd3ec95 |
✅ clear | 1 min | run |
f375e63 |
✅ clear | 4 min | run |
62d1025 |
✅ clear | 8 min | run |
6acc936 |
✅ clear | 2 min | run |
🩺 Aftercare
|
| Head | Result | Time | Check |
|---|---|---|---|
798ac75 |
✅ clear · 2 fixed | 6 min | run |
789763f |
🔴 2 open · 2 fixed · 2 new | 15 min | run |
048b82b |
🔴 2 open · 1 held · 1 new | 5 min | run |
6a1bafe |
🔴 1 open · 1 fixed · 1 new | 11 min | run |
189da4a |
🔴 1 open · 1 fixed · 1 new | 6 min | run |
bd3ec95 |
🔴 1 open · 2 fixed · 1 new | 9 min | run |
f375e63 |
🔴 2 open · 1 fixed · 2 new | 11 min | run |
62d1025 |
🔴 1 open · 3 fixed · 1 new | 17 min | run |
6acc936 |
🔴 3 open · 1 fixed · 2 held · 1 new | 8 min | run |
eb827a7 |
🔴 3 open · 3 fixed · 2 held · 1 new | 6 min | run |
…lures Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes
Fixed in eb827a7. A1: fix. Reload and Overwrite act on every live session of the file; new test covers a reopened panel. A3: fix. A deleted file no longer matches its loaded revision, so the save is refused. A4: fix. A5: fix. Stat and read failures stay effect-service-conventions-1: fix. Same change as A5. A2: drop the compare-to-write window is microseconds; the writers in the issue (agents, terminals) cannot be serialized, and a per-file lock would wrap upstream's |
…e drafts Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes
Fixed in 6acc936. A1: fix. Reload marks the file; any editor still holding the dropped draft, live or disposed, skips the save until an edit captures a new base. New test covers a closed panel's in-flight save. A2: fix. Your concurrent trials hold; my earlier drop was wrong. A6: fix. The session registry is gone; the Reload marker replaced it. |
…rop superseded drafts Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes
Fixed in 62d1025. A1: fix. A save whose contents differ from the file's current draft is dropped, so a disposed editor cannot write its draft after Reload and a fresh edit. The in-flight test now edits again after Reload. A2: fix. The lock keys on the real path; the concurrency test saves through a symlink alias. A7: fix. The lock is a layer decorator added by one inserted pipe line in |
Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes
A8: fixed in f375e63.
|
Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes
Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes
A11: fixed in 189da4a.
|
…nflict Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes
A12: fixed in 6a1bafe.
Out of scope: an unchecked write to a FIFO path still blocks.
|
Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes
A13: fixed in 048b82b.
|
Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes
Fixed in 8f154fa. A13, reopened: a saved draft is now settled
A14: removed the unused Checks:
|
…mers Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes
Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes
Fixed in 798ac75. A15: a late save no longer revives a Reload
A16: draft bases now use a CI on 789763f: Test Server 3 hit the job limit
Checks:
|
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
## Problem An open workspace file panel saves its buffer over disk changes it never saw. An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents. ## Change Each save names the disk version its draft started from; a stale save is refused, and the user is asked. 1. Panel loads the file; client keeps its revision 2. Save sends `expectedRevision` with the contents 3. Server re-reads the file and compares - one write per real file at a time 4. Changed or deleted: refused as `contents_changed` 5. Editor shows a persistent toast: - **Overwrite:** saves the draft without the check - **Reload:** drops draft, shows disk version - **Old drafts:** dropped or superseded, never saved **Revision:** length plus FNV-1a over the decoded text; synchronous, since `crypto.subtle` is missing on plain-HTTP remote origins. **Read failure:** stays `operation_failed`, never a conflict. **Unchanged:** saves with no loaded revision (new files, older clients) write as before. ### Layout ```text packages/contracts/src/ ├── projectFileRevision.fork.ts shared revision hash └── project.ts hooks: expectedRevision, contents_changed apps/server/src/ ├── workspace/WorkspaceFileSystem.fork.ts compare before write, write lock ├── workspace/WorkspaceFileSystem.ts hook: call the check ├── server.ts hook: add the write lock └── ws.ts hook: map the error apps/web/src/components/files/ ├── fileSaveBase.fork.ts per-file loaded revision ├── fileSaveConflict.fork.ts guarded write + toast ├── fileSaveCoordinator.ts hook: discard() ├── projectFilesQueryState.ts hooks: capture, forget └── useFileSaveCoordinator.ts hook: wrap the write ``` ### Surfaces - **Web editors:** plain, rendered, rich markdown - **Shared:** `useFileSaveCoordinator` saves all - **Desktop:** wraps web - **Mobile:** no workspace file editor - **Server:** `WorkspaceFileSystem.writeFile` only ### Retirement notes - **Hash:** duplicates upstream's private one - lives in `fileContentRevision.ts` - **Cherry-pick:** touches fork-only files - does not apply to upstream `main` as is - **Upstream:** fixed only the tab-close case - `pingdotgg#8630` - closed `pingdotgg#8475` ## Scope and approval Closes #1761 Human decision from the housekeeping walk: fix in the fork, ask on conflict. - [ ] **Disk change:** never lost to a stale buffer ## Verification Real Chrome, dev build of this branch, scratch project: 1. Panel loads `README.md` 2. Disk gains a line; user types in panel 3. Toast appears; disk keeps the new line ✅ 4. **Reload:** panel shows disk; toast closes ✅ 5. Disk changes again; user types; toast appears ✅ 6. **Overwrite:** disk holds the panel text ✅ https://github.com/user-attachments/assets/c1b44fba-f2c2-44f5-ae2e-9d2964570901 | Span | Real | Clip | | --- | --- | --- | | load and edit | 11s | 3x | | prompt and reload | 7s | kept | | idle | 85s | cut | | edit, prompt, overwrite | 13s | kept | **Tests:** each case fails without its fix and passes with it. - [x] Web: stale save asks, then overwrites - [x] Web: Reload drops the draft unwritten - [x] Web: Reload covers a reopened panel - [x] Web: Reload covers a closed panel's in-flight save - [x] Web: a save landing after Reload stays dropped - [x] Web: a fresh edit after Reload keeps it dropped - [x] Web: a saved draft is never resent unchecked - [x] Server: stale write refused - [x] Server: matching base saved - [x] Server: deleted file refused - [x] Server: file turned FIFO refused as not a file - [x] Server: racing saves via any link: one wins - [x] Server: unreadable file fails as a read - [x] Server: outside paths refused, never followed **Checks:** - [x] `vp run fork:ci` → exit 0 - [x] `apps/web` `vp run build` → exit 0 - [x] `apps/server` `vp run build` → exit 0 - [ ] `vp run check:changed` → exit 1 - all failures in files this PR does not touch - format: 3 mobile and server files - lint: `serviceLauncher.fork.test.ts` - test: `GitManager.test.ts` CLAUDE.md case Claude Opus 5.5, Claude Code in T3 Code. ## Fork trailers Squashes: - 40f0718 fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 716cede fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - 4bdc91e fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) - d161c26 fixup! fix(web): file saves no longer overwrite newer disk changes (#1869) (#1869) Fork-Domain: upstream-fixes Fork-Tier: bugfix Fork-Upstreamable: yes Co-authored-by: donjor <[email protected]>
Problem
An open workspace file panel saves its buffer over disk changes it never saw.
An agent or terminal edits the file, the user types, and autosave silently drops the newer disk contents.
Change
Each save names the disk version its draft started from; a stale save is refused, and the user is asked.
expectedRevisionwith the contentscontents_changedRevision: length plus FNV-1a over the decoded text; synchronous, since
crypto.subtleis missing on plain-HTTP remote origins.Read failure: stays
operation_failed, never a conflict.Unchanged: saves with no loaded revision (new files, older clients) write as before.
Layout
Surfaces
useFileSaveCoordinatorsaves allWorkspaceFileSystem.writeFileonlyRetirement notes
fileContentRevision.tsmainas ispingdotgg/t3code#8630pingdotgg/t3code#8475Scope and approval
Closes #1761
Human decision from the housekeeping walk: fix in the fork, ask on conflict.
Verification
Real Chrome, dev build of this branch, scratch project:
README.md1761-proof.mp4
Tests: each case fails without its fix and passes with it.
Checks:
vp run fork:ci→ exit 0apps/webvp run build→ exit 0apps/servervp run build→ exit 0vp run check:changed→ exit 1serviceLauncher.fork.test.tsGitManager.test.tsCLAUDE.md caseClaude Opus 5.5, Claude Code in T3 Code.
Fork trailers
Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes