Skip to content

fix(web): file saves no longer overwrite newer disk changes - #1869

Merged
donjor merged 12 commits into
hyprwsfrom
fix/1761-file-save-conflict
Oct 26, 2026
Merged

donjor merged 12 commits into
hyprwsfrom
fix/1761-file-save-conflict

Conversation

@donjor-agent

@donjor-agent donjor-agent Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

🤖 claude/anthropic claude-opus-5-5 (high)

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

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/t3code#8630
    • closed pingdotgg/t3code#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 ✅
1761-proof.mp4
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.

  • Web: stale save asks, then overwrites
  • Web: Reload drops the draft unwritten
  • Web: Reload covers a reopened panel
  • Web: Reload covers a closed panel's in-flight save
  • Web: a save landing after Reload stays dropped
  • Web: a fresh edit after Reload keeps it dropped
  • Web: a saved draft is never resent unchecked
  • Server: stale write refused
  • Server: matching base saved
  • Server: deleted file refused
  • Server: file turned FIFO refused as not a file
  • Server: racing saves via any link: one wins
  • Server: unreadable file fails as a read
  • Server: outside paths refused, never followed

Checks:

  • vp run fork:ci → exit 0
  • apps/web vp run build → exit 0
  • 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

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
@rsi-software-agent

rsi-software-agent Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

🧩 Effect Service Conventions 798ac75 ✅ clear

resolved 1

  • effect-service-conventions-1 Preserve filesystem failures in the stale-write guard ✅ eb827a7

🕐 26 Oct 12:04 UTC ⏱️ 2 min 🤖 gpt-6.1-sol 🪙 471.9K

⚠️ Not verified: Tests and runtime behavior were not executed.

Details

effect-service-conventions-1 Preserve filesystem failures in the stale-write guard · apps/server/src/workspace/WorkspaceFileSystem.fork.ts:74

  • Problem: Stat/read failures become contents_changed, discarding their causes and operation context.
  • Fix: Map stat/read failures to fork-owned tagged operation errors retaining their immediate cause.
  • Note: Stat/read failures now retain causes and operation context in WorkspaceFileSystemOperationError.
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

@rsi-software-agent

rsi-software-agent Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

🩺 Aftercare 798ac75 ✅ clear

resolved 16

  • A1 Make Reload cancel the active per-file save session ✅ 62d1025
  • A2 Serialize revision checking and writing for each resolved file ✅ 62d1025
  • A3 Distinguish deleted files from loaded empty files ✅ eb827a7
  • A4 Release conflict toast IDs when notifications are dismissed ✅ eb827a7
  • A5 Preserve filesystem failures in the stale-write guard ✅ eb827a7

commit eb827a7

  • A6 Give the per-file session registry an explicit lifecycle ✅ 6acc936

commit 6acc936

  • A7 Keep the serialization hook insertion-only ✅ 62d1025

commit 62d1025

  • A8 Validate paths before resolving write-lock keys ✅ f375e63

commit f375e63

  • A9 Serialize hard-link aliases by shared file identity ✅ bd3ec95
  • A10 Limit the lock-key fallback to missing files ✅ bd3ec95

commit bd3ec95

  • A11 Reject non-regular files before reading their revision ✅ 189da4a

commit 189da4a

  • A12 Do not offer Overwrite for non-regular targets ✅ 6a1bafe

commit 6a1bafe

  • A13 Stop reopened coordinators re-saving confirmed drafts ✅ 789763f

commit 048b82b

  • A14 Remove the unused raw change wrapper ✅ 789763f

commit 789763f

  • A15 Preserve Reload's settled state across late save completions ✅ 798ac75
  • A16 Stop retaining settled file atoms for the tab's lifetime ✅ 798ac75

🕐 26 Oct 12:11 UTC ⏱️ 6 min 🤖 gpt-6.1-sol 🪙 616.2K

⚠️ Not verified: Client boundary stubs; no browser/desktop E2E, live Windows, or external compare/write race verification.

Details

A1 Make Reload cancel the active per-file save session · apps/web/src/components/files/fileSaveConflict.fork.ts:86

  • Problem: Reload clears the base while a remounted coordinator can still save its stale draft unchecked.
  • Fix: Cancel active per-file saves before clearing the draft/base; test Reload after remount.
  • Note: Superseded-content check blocks the reproduced disposed-editor retry after Reload and a fresh edit.

A2 Serialize revision checking and writing for each resolved file · apps/server/src/workspace/WorkspaceFileSystem.ts:550

  • Problem: Concurrent clients can validate the same revision and silently overwrite each other's changes.
  • Fix: Serialize each resolved path's comparison and write; add a concurrent-client regression.
  • Note: Decorated service rejects the second save through identical, ./, and symlink paths.

A3 Distinguish deleted files from loaded empty files · apps/server/src/workspace/WorkspaceFileSystem.fork.ts:76

  • Problem: Missing files match empty-text revisions, allowing stale edits to undo external deletion silently.
  • Fix: Treat absence as a mismatch when expectedRevision is supplied; retain unchecked new-file writes.
  • Note: Missing targets now return null and cannot match the loaded empty-file revision.

A4 Release conflict toast IDs when notifications are dismissed · apps/web/src/components/files/fileSaveConflict.fork.ts:98

  • Problem: Dismissed toast IDs remain cached, so later conflicts update nonexistent notifications.
  • Fix: Delete cached IDs on close/removal so later refused saves can show fresh prompts.
  • Note: onRemove deletes the cached ID, allowing subsequent conflicts to create fresh prompts.

A5 Preserve filesystem failures in the stale-write guard · apps/server/src/workspace/WorkspaceFileSystem.fork.ts:75

  • Problem: Stat/read failures masquerade as conflicts and lose their causes; confirms effect-service-conventions-1.
  • Fix: Preserve structured stat/read failures and immediate causes; map them to operation_failed.
  • Note: Stat/read errors preserve operation and cause; transport retains operation_failed.

A6 Give the per-file session registry an explicit lifecycle · apps/web/src/components/files/fileSaveConflict.fork.ts:69

  • Problem: Normal teardown leaves session refs and file keys retained indefinitely in the global registry.
  • Fix: Register mounted owners; unregister settled sessions and delete empty file entries.
  • Note: The session registry and all render-time session registration were removed.

A7 Keep the serialization hook insertion-only · apps/server/src/workspace/WorkspaceFileSystem.ts:589

  • Problem: The serialization marker covers the upstream writer and fails the insertion-only reapplication rule.
  • Fix: Preserve upstream text and express lock ownership through insertion-only fork hooks.
  • Note: Upstream writer text is restored; new server wiring consists only of marked insertions.

A8 Validate paths before resolving write-lock keys · apps/server/src/workspace/WorkspaceFileSystem.fork.ts:137

  • Problem: Lock-key realpath resolves rejected UNC targets before validation, potentially exposing Windows credentials.
  • Fix: Validate workspace-relative containment before realPath; add a rejected-UNC regression.
  • Note: Traversal and UNC targets are rejected without realPath calls in the focused reproduction.

A9 Serialize hard-link aliases by shared file identity · apps/server/src/workspace/WorkspaceFileSystem.fork.ts:145

  • Problem: Hard-link aliases acquire separate locks; concurrent checked saves both succeed and lose one edit.
  • Fix: Key existing-file locks by device/inode, preserve missing-file behavior, and add a hard-link race regression.
  • Note: Five four-alias races each allowed one save; hard links now share the inode lock.

A10 Limit the lock-key fallback to missing files · apps/server/src/workspace/WorkspaceFileSystem.fork.ts:145

  • Problem: The blanket realPath fallback silently downgrades canonical locking after permission or I/O failures.
  • Fix: Keep the NotFound fallback; propagate other resolution failures as structured operation errors.
  • Note: Non-NotFound stat failure becomes an operation error with its immediate cause preserved.

A11 Reject non-regular files before reading their revision · apps/server/src/workspace/WorkspaceFileSystem.fork.ts:95

  • Problem: Replacing a loaded file with a FIFO blocks the revision read and holds its write lock indefinitely.
  • Fix: Reject non-regular stat types before reading; add a loaded-file-to-FIFO regression.
  • Note: Versioned save now rejects a FIFO without calling readFile; the original revision-read hang is resolved.

A12 Do not offer Overwrite for non-regular targets · apps/server/src/workspace/WorkspaceFileSystem.fork.ts:95

  • Problem: FIFO replacements become conflicts; Overwrite then blocks or reports success without saving a regular file.
  • Fix: Return WorkspacePathNotFileError for non-regular targets and cover the Overwrite follow-up.
  • Note: Non-regular versioned targets now return path_not_file and bypass the conflict prompt.

A13 Stop reopened coordinators re-saving confirmed drafts · apps/web/src/components/files/fileSaveConflict.fork.ts:70

  • Problem: A reopened coordinator replays a confirmed draft without a revision, overwriting newer disk contents.
  • Fix: Require a matching unsaved draft before dispatch; retire already-confirmed coordinator work.
  • Note: SETTLED survives successful readback; the reopened coordinator no longer replays its confirmed draft.

A14 Remove the unused raw change wrapper · apps/web/src/components/files/useFileSaveCoordinator.fork.browser.test.tsx:101

  • Problem: openEditor exposes an unused change wrapper, unnecessarily expanding its private fixture interface.
  • Fix: Remove the returned change property; no test calls it.
  • Note: Removed the unused returned change property; the internal edit callback remains.

A15 Preserve Reload's settled state across late save completions · apps/web/src/components/files/fileSaveConflict.fork.ts:71

  • Problem: Late save success resets Reload's marker and writes a closed editor's discarded draft.
  • Fix: Preserve settled state on late completions; test successful in-flight saves across Reload.
  • Note: Late success preserves SETTLED; the original reproduction no longer dispatches the discarded draft.

A16 Stop retaining settled file atoms for the tab's lifetime · apps/web/src/components/files/fileSaveBase.fork.ts:10

  • Problem: Settled entries permanently retain every saved or reloaded file atom.
  • Fix: Use weak keys so unused file atoms and their settled entries can collect.
  • Note: WeakMap replaces strong ownership; the original GC reproduction now collects unused settled atoms.
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
@donjor-agent

donjor-agent Bot commented Oct 26, 2026

Copy link
Copy Markdown
Author

🤖 claude/anthropic claude-opus-5-5 (high)

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. onRemove releases the toast ID; a dismissed prompt shows again on the next conflict.

A5: fix. Stat and read failures stay WorkspaceFileSystemOperationError (operation_failed) with their cause.

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 writeFile body at fork-sync and retirement cost.

…e drafts

Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes
@donjor-agent

donjor-agent Bot commented Oct 26, 2026

Copy link
Copy Markdown
Author

🤖 claude/anthropic claude-opus-5-5 (high)

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. oneWritePerFile serializes compare and write per file through one Effect.fn combinator hook. New test: two saves from one disk version, one refused.

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
@donjor-agent

donjor-agent Bot commented Oct 26, 2026

Copy link
Copy Markdown
Author

🤖 claude/anthropic claude-opus-5-5 (high)

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 server.ts; writeFile is upstream text again.

Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes
@donjor-agent

donjor-agent Bot commented Oct 26, 2026

Copy link
Copy Markdown
Author

🤖 claude/anthropic claude-opus-5-5 (high)

A8: fixed in f375e63.

  • Lock checks with resolveRelativePathWithinRoot
  • Refused path: no realPath; upstream refuses it
  • Test: ../ and UNC refused, never followed

Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes
@donjor-agent

donjor-agent Bot commented Oct 26, 2026

Copy link
Copy Markdown
Author

🤖 claude/anthropic claude-opus-5-5 (high)

A9: fixed in bd3ec95.

  • Lock key: device and inode from stat
  • Hard and symbolic links share one lock
  • Test: three names race; one save wins

A10: fixed in bd3ec95.

  • Missing file: path key, as before
  • Other stat failure: operation error

Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes
@donjor-agent

donjor-agent Bot commented Oct 26, 2026

Copy link
Copy Markdown
Author

🤖 claude/anthropic claude-opus-5-5 (high)

A11: fixed in 189da4a.

  • Not a regular file: refused as changed, unread
  • Test: loaded file becomes a FIFO; save refused
  • Before the fix, that test hung on the read

…nflict

Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes
@donjor-agent

donjor-agent Bot commented Oct 26, 2026

Copy link
Copy Markdown
Author

🤖 claude/anthropic claude-opus-5-5 (high)

A12: fixed in 6a1bafe.

  • Non-regular target: WorkspacePathNotFileError
  • Maps to path_not_file; no Overwrite prompt
  • Test: file turned FIFO is refused as not a file

Out of scope: an unchecked write to a FIFO path still blocks.

  • Upstream's write opens with O_WRONLY
  • Same before this PR; nothing here leads to it

Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes
@donjor-agent

donjor-agent Bot commented Oct 26, 2026

Copy link
Copy Markdown
Author

🤖 claude/anthropic claude-opus-5-5 (high)

A13: fixed in 048b82b.

  • Sends only a draft still held unsaved
  • Saved by another editor: dropped, not resent
  • Test: second editor recovers an in-flight draft
  • Test file no longer stubs draft recovery

Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes
@donjor-agent

donjor-agent Bot commented Oct 26, 2026

Copy link
Copy Markdown
Author

🤖 claude/anthropic claude-opus-5-5 (high)

Fixed in 8f154fa.

A13, reopened: a saved draft is now settled

  • Confirm and Reload both mark the file settled
  • A settled save is discarded, never sent
  • The next edit takes a fresh base from disk
  • One check replaces the reload and A13 checks
  • Test now clears the draft before the replay
    • fails without the fix: two writes

A14: removed the unused change wrapper.

Checks:

  • web file tests: 103 pass
  • web vp run typecheck → exit 0
  • vp lint → exit 0
  • vp run fork:ci → exit 0

…mers

Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes
Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes
@donjor-agent

donjor-agent Bot commented Oct 26, 2026

Copy link
Copy Markdown
Author

🤖 claude/anthropic claude-opus-5-5 (high)

Fixed in 798ac75.

A15: a late save no longer revives a Reload

  • Rebase skips a settled file
  • The closed panel's follow-up is discarded
  • Test: in-flight save succeeds after Reload
    • fails without the fix: dropped draft written

A16: draft bases now use a WeakMap.

CI on 789763f: Test Server 3 hit the job limit

  • Chromium install took 14 of 20 minutes
  • No test failed; this push reruns it

Checks:

  • web file tests: 104 pass
  • web vp run typecheck → exit 0
  • vp lint → exit 0
  • vp run fork:ci → exit 0

@donjor
donjor merged commit d614a0e into hyprws Oct 26, 2026
13 checks passed
@donjor
donjor deleted the fix/1761-file-save-conflict branch October 26, 2026 12:23
donjor added a commit that referenced this pull request Oct 27, 2026
## 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]>
donjor added a commit that referenced this pull request Oct 27, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 8, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 9, 2026
## 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]>
donjor added a commit that referenced this pull request Nov 9, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 9, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 9, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 9, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 9, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 10, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 10, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 10, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 10, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 10, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 11, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 11, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 11, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 11, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 11, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 11, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 12, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 12, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 12, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 12, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 13, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 13, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 13, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 13, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 14, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 14, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 14, 2026
## 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]>
rsi-software-agent Bot pushed a commit that referenced this pull request Nov 14, 2026
## 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workspace file edits silently overwrite newer disk changes [📍]

1 participant