Repository navigation
Conversation
…a conflict choice A project has more writers than the editor: an agent over the API, Ask and Auto-cut on the server, a teammate, the same person in another tab. Saves were last-write-wins and the editor never re-read the project, so the next edit in an open editor silently erased whatever had been saved elsewhere. - Every write adds one to edit_projects.revision. PUT, instruct and autocut take an optional revision and answer 409 conflict when it is stale; their writes are guarded on the revision they read, so an AI pass that took a minute cannot overwrite a save made meanwhile. - GET /api/projects/:id carries the revision as an ETag. An open editor asks every 5 s while visible (an empty 304 when nothing changed) and shows a newer version at once when it has nothing unsaved, as one undo step. - With unsaved edits it stops saving and asks: show the saved version (the cut on screen stays one undo away) or keep mine. - Saves run one at a time; Ask and Auto-cut save what is on screen first. - Going back to Projects or closing the tab inside the 700 ms wait no longer drops the last edit. - agent.md: send back the revision you read.
… the document under a held drag Not every storage this app can run on reports a changes count, and a missing count reads as 0, which would have refused every save. A drag holds an element's index from press to release, so the editor no longer takes a newer version while a pointer is down. A save to a deleted project answers 404.
# Conflicts: # agent.md # schema.sql # src/client/edit.tsx
# Conflicts: # agent.md # schema.sql # src/client/edit.tsx
Member
Author
|
Merged main (8e3ae80) in. Conflicts: agent.md and schema.sql took both sides; edit.tsx kept this branch's persistence, which already saves to the editor's own project on the way out and is now one editor per project (main's key). One fix the merge needed: asking for highlights with a new brief now adds a revision, so an open editor picks up the brief instead of saving its old one over it. Typecheck, 163 tests, client build. |
# Conflicts: # src/server/index.ts
This was referenced Oct 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
A project has more writers than the editor: an agent working over the API, Ask and Auto-cut on the server, a teammate, the same person in a second tab. Saves were last-write-wins, and an open editor never re-read the project. So if an agent changed the cut while the editor was open, the next click in the editor saved the old document over it, and nobody saw it happen. Ask and Auto-cut also wrote back a document they had read up to a minute earlier. And going back to Projects, or closing the tab, within 700 ms of an edit dropped that edit.
What changes
edit_projects.revision(an additiveALTER TABLE ... ADD COLUMN).PUT /api/projects/:id,instructandautocuttake an optionalrevisionand answer409 { error: "conflict", revision }when it is stale. Writes are guardedWHERE revision = <read>and read the result fromRETURNING, so concurrent saves on one base produce exactly one winner, and an AI pass cannot overwrite a save made while it ran. The guard reads rows rather than thechangescount because not every storage backend@clawnify/dbsupports reports a count, and a missing count reads as 0, which would refuse every save. Leavingrevisionout keeps the old behaviour, so existing callers still work.GET /api/projects/:idsends the revision as anETag. An open editor checks every 5 s while the tab is visible, and gets an empty304when nothing changed. With nothing unsaved, a newer version shows up straight away as one undo step, so the person sees an agent's change land and can undo it.agent.md: agents send back therevisionthey read, and re-read on a 409.Verified
tsc,vitest(92, including the newtest/sync.test.ts),vite build.HTTP against local D1 +
wrangler dev, 12/12: ETag/304, stale PUT returns 409 and changes nothing, a PUT without revision still wins, a stale instruct is refused before any model call, 6 concurrent PUTs on one base give exactly one 200.In a browser on the same local stack (the held-pointer check: no swap for 8 s while held, taken on release):
Deployed to a test instance:
the schema reconcile added the column to the existing database (old projects read
revision: 0);stale PUT and stale instruct return 409;
the 304 passes through the app router;
an agent-style save showed up in the open editor once the tab was visible, and the next checks returned 304s.
after the RETURNING change, 6 concurrent PUTs on real D1 gave one 200 and five 409s, and a PUT to a deleted project returns 404.
The deployed run turned up one thing, fixed in the second commit. The edge compresses the JSON and hands the ETag back weakened (
W/"0"), so a client echoing it got a full 200.If-None-Matchnow uses weak comparison, as HTTP defines for that header.Known edges