Fix: settings.json/auth.json can silently lose all settings or credentials on a crash or race (#983) #1765
Replies: 2 comments
|
This was already diagnosed in #983 and implemented in #989, with tests. I opened both on Aug 8. The PR was closed in the backlog sweep, not because the fix was wrong. This discussion restates that writeup and ships the same three fixes. A few things that aren't independent rediscovery:
The comment on #983 calls this "root cause + tested fixes" reproduced fresh against main. It does not mention #989 at all. The extra tests (read-only no-create, cleanup failure, second-run no-op) are real deltas. Fine. That's not a new finding. If this goes anywhere, credit #983 / #989 and @Adolanium. |
|
Correction and credit, after @Adolanium's comment above. The diagnosis in #983 and the implementation in #989 are yours, from Aug 8, and predate this discussion. I should have led with that instead of citing #989 as "an earlier PR attempt." Re-checked the two diffs side by side: the three root causes (the settings.json first-write race, the non-atomic auth.json write, the destructive migration ordering) and the fix approach for each (re-read under the lock before trusting a first-time write, temp-file + rename for auth.json, write auth.json durably before touching either legacy source) are the same analysis as #989, arrived at independently in implementation detail (different temp-file naming, different write primitives) but not in substance or root cause. For the record: #989 (and you) should be the credited source if a maintainer picks this up, not this discussion. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
Three related crash/race problems in how
settings.jsonandauth.jsonare written — this is #983, one of the "related reports" under the still-open tracker #1380 (Make persisted session and configuration state crash-safe). All three are still live onmain(06860844e), confirmed with new tests that fail against the pre-fix code.1.
FileSettingsStorage.withLock(settings-manager.ts) — concurrent first-write raceThe lock was only acquired when the target file already existed. When two processes both create a project
settings.jsonfor the first time, both computefn(current=undefined)from an unlocked read; whichever locks and writes second silently overwrites the first, discarding its settings.Fix: on the first-write path (file didn't exist when we started), re-read under the lock once it's actually acquired. If the on-disk state changed since the unlocked read (another writer landed first), re-invoke
fnagainst the real current state instead of the staleundefinedsnapshot — every currentfnis a pure merge of its argument, so re-invoking is safe. Read-only calls (fnalways returnsundefined, e.g. the startup load path) still never lock or create the directory. (An earlier version of this fix did that unconditionally and brokeshould not create .pi folder when only reading project settings, an existing deliberate test — worth flagging since it's an easy trap.)2.
FileAuthStorageBackend.withLock/withLockAsync(auth-storage.ts) — non-atomic writeWrote
auth.jsonin place (writeFileSyncdirectly on the target). This file holds every stored OAuth refresh token and API key, so a crash mid-write corrupts all of them at once, not just the record being written.Fix: same temp-file + rename pattern
settings-manager.tsalready used, via a new privatewriteAtomic(), used for both the real write andensureFileExists()'s own first-touch placeholder.3.
migrateAuthToAuthJson(migrations.ts) — destructive migration orderingRenamed
oauth.json→oauth.json.migratedand deletedsettings.json'sapiKeysbeforeauth.jsonwas written. A crash in between left nothing to migrate on the next run (oauth.jsonis gone,apiKeysis gone,auth.jsonwas never created) — every provider credential effectively lost.Fix: collect credentials from both legacy sources first without mutating either, write
auth.jsondurably (temp file + rename) if anything was collected, and only then clean up the sources. A crash after the durable write is safe: the next run seesexistsSync(authPath)and skips migration, leaving the sources as harmless untouched leftovers instead of lost data.Tests
packages/coding-agent/test/settings-manager.test.ts: new "concurrent first-write race (Settings and credential files can be silently lost: non-atomic writes and unsafe auth migration order #983)" block. Simulates the race deterministically (no real threads/timing) by having process A'sfncallback itself trigger process B's write mid-computation, then asserting A re-derives from B's real state instead of clobbering it — plus a "read-only call does not lock or create the directory" test guarding the regression mentioned above.packages/coding-agent/test/auth-storage-atomic-write.test.ts(new): mocksfs(same pattern as the existingsession-manager-flush.test.ts) to assertwithLock/withLockAsync/ensureFileExistsall go through temp file + rename, neverwriteFileSyncdirectly onauth.json.packages/coding-agent/test/migrations-auth-atomic.test.ts(new): happy-path migration correctness, a call-order assertion thatauth.jsonis written before either legacy source is touched (fails on pre-fix code), a non-fatal cleanup-step-failure case, and idempotent no-op onceauth.jsonexists.All new tests verified to fail against the pre-fix code where the bug they target is reachable.
Validation
npx tsgo -p tsconfig.json --noEmitand the rootnpm run check(biome, tsgo, installer render, browser smoke) both pass. Added apackages/coding-agent/.changes/fragment per the changelog-fragment CI check.Patch
Branch: https://github.com/kaluli123123/prime-agent/tree/fix/settings-auth-atomic-writes
Diff: main...kaluli123123:prime-agent:fix/settings-auth-atomic-writes
Happy to open a PR from this branch if a maintainer wants to invite implementation, per CONTRIBUTING.md.
All reactions