You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Use provider-aware staging and identity-checked no-clobber publication with explicit recovery receipts. Preserve original source; never delete a target adopted by another process. OpenCode import uncertainty needs its native API contract.
Source finding and impact
Claude and Codex target writers call writeFile on final discoverable native filenames. Full in-memory projection validation does not prevent an interruption or partial write from leaving an incomplete target under that name.
Evidence and environment
Inspected source at 552914f5610518458f944fa1bdaf63f243685bd1 on 2026-09-12 in the local macOS workspace. This finding has not yet been reproduced through a running application or the package integration suite.
Inject partial writes, failure before publish, publish-success/receipt-failure and destination collision in temporary provider roots. Discovery must not see an incomplete final artifact; recovery must recognize completed publication without duplicate creation.
The resolving PR must protect the observable contract and document any remaining uncertainty. This planning pass makes no runtime changes.
Reopening: PR #1076 merged with Fixes #928 on its first commit, but it delivers only part of this issue.
Delivered — atomic, no-clobber publication. Staged in the target directory, fsynced, published with link (atomic create-exclusive, which also refuses to follow a symlink at the target). An existing target is compared by size-then-streamed-hash: identical is idempotent success, different refuses rather than destroying a session another process adopted. A filesystem without hard links falls back to wx on the final name, keeping no-clobber and giving up only the zero-width window, which is still strictly better than the writeFile truncate this replaced. Abandoned stages are swept, because nothing else in the system would ever remove them and a stage that outlives a failed rm is a second hard link that pins the inode's bytes forever.
Not delivered — the recovery receipt. This issue asks that "recovery must recognize completed publication without duplicate creation", and review established why that cannot work today: every entry point (switchProvider, duplicateSession, rewindSession, stripCodexCyberPolicy) mints a fresh randomUUID() immediately before projecting, and the projector stamps it on every row. So a retry after "published but the bookkeeping died" produces a new uuid → a new filename → a duplicate session file. There is no retry wrapper between the IPC handler and the writer to hang a stable id on.
Closing that needs a stable retry identity threaded from whatever would do the retrying. The already-published path exists and is tested; it simply has no caller that can reach it.
Also untouched: OpenCode import uncertainty. OpenCode publishes through opencode import, a CLI the app does not control, so it has no equivalent staging seam.
Fixed on main and never closed — the PR carried no Closes trailer.
422ca6fdfix(provider-switch): publish a native transcript atomically, never clobbering, plus a0a4538a (fall back where hard links do not exist, and sweep stages).
Both native writers called writeFile on the final discoverable name, and writeFile truncates before it streams — so a crash mid-write left a truncated transcript under exactly the name the provider enumerates and resumes from. Publication now goes through link, chosen over rename deliberately: rename is atomic but clobbers, and the target name embeds a session id, so overwriting it is precisely the "never destroy a target another process adopted" rule this issue asked for. link either creates the name or fails EEXIST, with no window in between.
Covered by src/main/providerSwitch/atomicPublish.system.test.ts.
Found by cross-referencing open issues against commit messages on main.
Activity
fix(provider-switch): fall back where hard links do not exist, and sw…
Juliusolsson05 commentedon Sep 20, 2026
Reopening: PR #1076 merged with
Fixes #928on its first commit, but it delivers only part of this issue.Delivered — atomic, no-clobber publication. Staged in the target directory, fsynced, published with
link(atomic create-exclusive, which also refuses to follow a symlink at the target). An existing target is compared by size-then-streamed-hash: identical is idempotent success, different refuses rather than destroying a session another process adopted. A filesystem without hard links falls back towxon the final name, keeping no-clobber and giving up only the zero-width window, which is still strictly better than thewriteFiletruncate this replaced. Abandoned stages are swept, because nothing else in the system would ever remove them and a stage that outlives a failedrmis a second hard link that pins the inode's bytes forever.Not delivered — the recovery receipt. This issue asks that "recovery must recognize completed publication without duplicate creation", and review established why that cannot work today: every entry point (
switchProvider,duplicateSession,rewindSession,stripCodexCyberPolicy) mints a freshrandomUUID()immediately before projecting, and the projector stamps it on every row. So a retry after "published but the bookkeeping died" produces a new uuid → a new filename → a duplicate session file. There is no retry wrapper between the IPC handler and the writer to hang a stable id on.Closing that needs a stable retry identity threaded from whatever would do the retrying. The
already-publishedpath exists and is tested; it simply has no caller that can reach it.Also untouched: OpenCode import uncertainty. OpenCode publishes through
opencode import, a CLI the app does not control, so it has no equivalent staging seam.docs(decomposition): record the sweep through 2026-09-20 13:05Z
docs(decomposition): record the sweep through 2026-09-20 13:45Z
Juliusolsson05 commentedon Sep 20, 2026
Fixed on main and never closed — the PR carried no
Closestrailer.422ca6fdfix(provider-switch): publish a native transcript atomically, never clobbering, plusa0a4538a(fall back where hard links do not exist, and sweep stages).Both native writers called
writeFileon the final discoverable name, andwriteFiletruncates before it streams — so a crash mid-write left a truncated transcript under exactly the name the provider enumerates and resumes from. Publication now goes throughlink, chosen overrenamedeliberately:renameis atomic but clobbers, and the target name embeds a session id, so overwriting it is precisely the "never destroy a target another process adopted" rule this issue asked for.linkeither creates the name or failsEEXIST, with no window in between.Covered by
src/main/providerSwitch/atomicPublish.system.test.ts.Found by cross-referencing open issues against commit messages on main.