Skip to content

feat: crash-safe atomic+fsync writes for all persistent state - #99

Merged
TraderSamwise merged 4 commits into
masterfrom
feat/atomic-durable-writes
Jun 7, 2026
Merged

feat: crash-safe atomic+fsync writes for all persistent state#99
TraderSamwise merged 4 commits into
masterfrom
feat/atomic-durable-writes

Conversation

@TraderSamwise

@TraderSamwise TraderSamwise commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Problem (epic W1)

State writes across aimux were not crash-safe:

  • No fsync anywhere — even the "atomic" tmp+rename writers (topology, exchange, metadata) could lose the rename on power loss because nothing was flushed to disk.
  • Plain writeFileSync (corruptible mid-write) on state.json, config, team, registry, last-used, credentials, service snapshots, attachments, ui-state, etc.
  • Silent reset-on-corrupt — a torn file quietly became empty defaults, then got overwritten, masking the loss.

Change

Phase 1 — one durable primitive (src/atomic-write.ts): atomicWrite(path, string|Buffer, {mode?}) = mkdir → write temp → fsync(temp) → rename → fsync(dir), temp cleaned on failure. writeJsonAtomic / writeTextAtomic build on it. Directory fsync is best-effort (some filesystems reject it); file-fsync errors propagate.

Phase 2 — route durable writers through it: both authority YAML stores (topology, exchange), state.json, project+global config, team config, projects registry, last-used, service snapshots, credentials (keeps 0o600, no world-readable window), attachments (binary + json), ui-state, metadata endpoint text, project-takeover. Regenerable high-frequency projections (statusline, live.md, status text) are intentionally left as-is — fsync there is cost without durability gain.

Phase 3 — quarantine-on-corrupt: quarantineCorruptFile() moves a bad authority file to <path>.corrupt-<ts> and logs it, instead of silently resetting. Wired into the readers for state.json, config, registry, and metadata. Once quarantined the file is gone, so hot readers fall through to the fallback without a quarantine storm.

Verification

  • yarn verify green: typecheck + lint clean, 1016/1016 tests (new atomic-write.test.ts: primitive, mode, verbatim text, overwrite, quarantine, missing-file).
  • Independent implementation audit: PASS on fsync sequence, credentials security, YAML-store lock interaction, quarantine no-storm, byte-exactness, imports.

Scope

Independent of the other epics. Does not change authority/ownership semantics (W2) or the CLI surface (W3).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Safer, durable atomic file writes across the app to reduce risk of data loss.
  • Bug Fixes

    • Corrupt JSON/state/config files are quarantined instead of silently ignored, preserving originals for recovery.
  • Updates

    • Persistence for attachments, credentials, UI state, metadata, runtime state, and project/team data now use atomic/durable writes.
  • Tests

    • Added tests validating atomic write behaviors and quarantine handling.

test and others added 3 commits June 7, 2026 09:41
Atomic tmp+rename without fsync can still lose the write (or the rename)
on power loss, because the kernel may not have flushed either to disk.
Add a single durable write path used by all persistent state:

- atomicWrite(path, string|Buffer, {mode?}): mkdir, write temp, fsync the
  temp file, rename into place, then fsync the directory so the rename
  survives a crash. Temp file is cleaned up on failure.
- writeJsonAtomic delegates to it; add writeTextAtomic for text/yaml.
- directory fsync is best-effort (some filesystems reject it); file fsync
  errors propagate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace bare writeFileSync / hand-rolled tmp+rename with the crash-safe
atomicWrite primitive across persistent state:

- authority stores: runtime-topology.yaml and runtime-exchange.yaml now
  fsync (file + dir) instead of tmp+rename without flush
- state.json (lifecycle + persistence), project + global config, team
  config, projects registry, last-used, service-state snapshots
- credentials (auth.json) keeps mode 0o600 with no world-readable window
  (the temp file is created 0o600; redundant chmod removed)
- attachments (binary content + json metadata), dashboard ui-state,
  metadata endpoint text, dashboard client prefs, project-takeover state

Regenerable high-frequency projections (statusline, live.md, status text)
are intentionally left as-is; fsync there is cost without durability gain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A torn or unparseable authority file previously reset to empty on read,
and the next write overwrote the evidence — silent data loss. Add
quarantineCorruptFile(path): move the bad file to <path>.corrupt-<ts>,
log it, then return the empty fallback. Wired into the authority readers:
state.json, project + global config, projects registry, and metadata.

Once quarantined the file no longer exists, so reads fall through to the
fallback without re-triggering (no quarantine storm on hot readers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment Jun 7, 2026 2:36am

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f456163-eabb-4798-abed-a9b9d74057d5

📥 Commits

Reviewing files that changed from the base of the PR and between e4b7aa4 and 8bdd3bb.

📒 Files selected for processing (2)
  • src/multiplexer/runtime-lifecycle-methods.ts
  • src/multiplexer/service-state-snapshot.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/multiplexer/service-state-snapshot.ts
  • src/multiplexer/runtime-lifecycle-methods.ts

📝 Walkthrough

Walkthrough

Adds a durable, crash-safe atomic write utility (atomicWrite, writeJsonAtomic, writeTextAtomic) and quarantine helper (quarantineCorruptFile), then replaces many direct synchronous file writes across config, state, metadata, attachments, and runtime stores to use those helpers.

Changes

Atomic File Write Refactoring

Layer / File(s) Summary
Core atomic-write implementation and tests
src/atomic-write.ts, src/atomic-write.test.ts
Implements atomicWrite() with fsync and directory fsync, writeJsonAtomic()/writeTextAtomic() delegating to it, and quarantineCorruptFile(); tests validate durability, newline semantics, mode handling, overwrites, and quarantine behavior.
Config and credentials persistence
src/config.ts, src/credentials.ts
Project/global config now saved via writeJsonAtomic() and quarantines corrupt files on parse failure; credentials saved atomically via atomicWrite() (mode enforced) replacing manual write+chmod.
Application UI, last-used, and metadata
src/dashboard/ui-state-store.ts, src/metadata-server.ts, src/metadata-store.ts, src/last-used.ts
Dashboard snapshots, client preferences, last-used state, and metadata endpoint text now written with atomic JSON/text writers; metadata/state load paths quarantine corrupt files before fallback.
Attachment storage
src/attachment-store.ts
Attachment binary blobs written with atomicWrite() and metadata saved with writeJsonAtomic(), replacing direct synchronous writes.
Multiplexer state and services
src/multiplexer/persistence-methods.ts, src/multiplexer/runtime-lifecycle-methods.ts, src/multiplexer/service-state-snapshot.ts, src/multiplexer/services.ts
Service lists, saved state, and snapshots use writeJsonAtomic() for persistence; load handlers quarantine corrupt state files prior to returning defaults.
Runtime-core exchange/topology stores
src/runtime-core/exchange-store.ts, src/runtime-core/topology-store.ts
Replaces manual tmp-file+rename persistence with shared atomicWrite() while preserving existing serialization.
Project registry, takeover, and team config
src/paths.ts, src/project-takeover.ts, src/team.ts
Projects registry quarantines corrupt projects.json and saves atomically; project takeover and team config writes use writeJsonAtomic().

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant atomicWrite
  participant TempFile
  participant Filesystem
  participant ParentDir
  Caller->>atomicWrite: write(path, data, {mode?})
  atomicWrite->>TempFile: create unique .tmp and write bytes
  TempFile->>Filesystem: fsync(temp fd)
  atomicWrite->>Filesystem: close(temp fd)
  atomicWrite->>Filesystem: rename .tmp -> final path
  Filesystem->>ParentDir: fsync(parent directory)
  ParentDir-->>Caller: return / throw
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • TraderSamwise/aimux#30: Overlaps runtime topology/exchange store changes where the shared atomic write helper is used.

Poem

"I hopped with a pen and a tiny clipboard,
Temp files turned gentle, renamed without slip,
Corrupt crumbs tucked in a .corrupt- den,
Durability stitched by a rabbit's quick kin,
Hooray — safe writes! 🐰"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing crash-safe atomic+fsync writes for persistent state across the codebase, which is the primary focus of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/atomic-durable-writes

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/multiplexer/service-state-snapshot.ts (1)

77-83: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Quarantine corrupt state file before treating as null.

When JSON.parse fails here, the corrupt state file is silently discarded and overwritten with fresh snapshot data (line 86). This loses potentially valuable debugging information. Per the PR objectives, quarantine should be wired into readers for state.json.

Add quarantineCorruptFile(statePath) in the catch block to preserve the corrupt file before falling back to null.

🛡️ Suggested fix
     try {
       existing = JSON.parse(readFileSync(statePath, "utf-8")) as SavedState;
     } catch {
+      quarantineCorruptFile(statePath);
       existing = null;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/multiplexer/service-state-snapshot.ts` around lines 77 - 83, When
JSON.parse of the saved state at statePath fails in the try/catch that assigns
to existing (SavedState), call quarantineCorruptFile(statePath) inside the catch
before setting existing = null so the corrupt state.json is preserved for
debugging; update the catch block in the read/parse logic that uses
existsSync(statePath) / readFileSync to invoke quarantineCorruptFile(statePath)
and then fall back to existing = null.
🧹 Nitpick comments (7)
src/paths.ts (1)

413-417: 💤 Low value

Optional: Remove redundant directory check.

The directory creation at lines 414–415 is redundant because writeJsonAtomic ensures the parent directory exists via its internal mkdirSync(dirname(path), { recursive: true }) call.

♻️ Proposed simplification
 function saveRegistry(registry: ProjectsRegistry): void {
-  const dir = getGlobalAimuxDir();
-  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
   writeJsonAtomic(getProjectsRegistryPath(), registry);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/paths.ts` around lines 413 - 417, The saveRegistry function currently
does an explicit existsSync/mkdirSync on getGlobalAimuxDir() before calling
writeJsonAtomic; remove the redundant directory check and creation and rely on
writeJsonAtomic (which already calls mkdirSync(dirname(path), { recursive: true
}) internally). Update saveRegistry to simply call
writeJsonAtomic(getProjectsRegistryPath(), registry) and remove the
getGlobalAimuxDir/existsSync/mkdirSync code paths to keep the function concise.
src/config.ts (2)

243-249: 💤 Low value

Optional: Remove redundant directory check.

Same as saveConfig above—the directory creation at lines 244–246 is redundant because writeJsonAtomic ensures the parent directory exists.

♻️ Proposed simplification
 export function saveGlobalConfig(config: Partial<AimuxConfig>): void {
-  const dir = getGlobalAimuxDir();
-  if (!existsSync(dir)) {
-    mkdirSync(dir, { recursive: true });
-  }
   writeJsonAtomic(getGlobalConfigPath(), config);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config.ts` around lines 243 - 249, In saveGlobalConfig, remove the
redundant directory existence check and mkdirSync (the block using
getGlobalAimuxDir and existsSync/mkdirSync) and simply call
writeJsonAtomic(getGlobalConfigPath(), config); rely on writeJsonAtomic to
ensure the parent directory exists; update saveGlobalConfig to only compute
getGlobalConfigPath() and call writeJsonAtomic to simplify the function
(references: saveGlobalConfig, getGlobalAimuxDir, getGlobalConfigPath,
writeJsonAtomic).

234-240: 💤 Low value

Optional: Remove redundant directory check.

The existsSync and mkdirSync calls at lines 236–238 are redundant because writeJsonAtomic delegates to atomicWrite, which already creates the parent directory via mkdirSync(dirname(path), { recursive: true }). Removing these lines would simplify the code without changing behavior.

♻️ Proposed simplification
 export function saveConfig(config: AimuxConfig): void {
-  const dir = getLocalAimuxDir();
-  if (!existsSync(dir)) {
-    mkdirSync(dir, { recursive: true });
-  }
   writeJsonAtomic(getConfigPath(), config);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config.ts` around lines 234 - 240, The saveConfig function currently
creates the local dir before calling writeJsonAtomic, which is redundant because
writeJsonAtomic/atomicWrite already ensures the parent directory exists; remove
the unused const dir = getLocalAimuxDir() and the if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true }); } block and leave only
writeJsonAtomic(getConfigPath(), config) inside saveConfig (refer to saveConfig,
getLocalAimuxDir, writeJsonAtomic, getConfigPath).
src/team.ts (2)

127-133: 💤 Low value

Optional: Remove redundant directory check.

The directory creation at lines 128–131 is redundant because writeJsonAtomic ensures the parent directory exists.

♻️ Proposed simplification
 export function saveTeamConfig(config: TeamConfig): void {
-  const dir = getLocalAimuxDir();
-  if (!existsSync(dir)) {
-    mkdirSync(dir, { recursive: true });
-  }
   writeJsonAtomic(getProjectTeamPath(), config);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/team.ts` around lines 127 - 133, The saveTeamConfig function currently
computes dir via getLocalAimuxDir() and performs an existsSync/mkdirSync check
before calling writeJsonAtomic; remove the redundant directory check and the
unused dir variable and simplify saveTeamConfig to just call
writeJsonAtomic(getProjectTeamPath(), config) because writeJsonAtomic already
ensures the parent directory exists; keep the function name saveTeamConfig and
calls to getProjectTeamPath and writeJsonAtomic unchanged.

138-144: 💤 Low value

Optional: Remove redundant directory check.

Same as saveTeamConfig above—the directory creation at lines 139–142 is redundant because writeJsonAtomic ensures the parent directory exists.

♻️ Proposed simplification
 export function saveGlobalTeamConfig(config: TeamConfig): void {
-  const dir = getGlobalAimuxDir();
-  if (!existsSync(dir)) {
-    mkdirSync(dir, { recursive: true });
-  }
   writeJsonAtomic(getGlobalTeamPath(), config);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/team.ts` around lines 138 - 144, The directory existence check and
mkdirSync calls inside saveGlobalTeamConfig are redundant because
writeJsonAtomic already ensures the parent directory exists; remove the block
that calls existsSync(getGlobalAimuxDir()) and mkdirSync(...), leaving
saveGlobalTeamConfig to simply call writeJsonAtomic(getGlobalTeamPath(), config)
(matching the behavior of saveTeamConfig).
src/credentials.ts (2)

102-105: 💤 Low value

Consider removing the redundant mkdirSync call.

writeJsonAtomicatomicWrite already calls mkdirSync(dirname(path), { recursive: true }) internally, so the explicit mkdirSync(dir, { recursive: true }) on line 104 is now redundant.

♻️ Proposed simplification
 function persistLastUsedState(projectRoot: string, state: LastUsedState): void {
-  const dir = getProjectStateDirFor(projectRoot);
-  mkdirSync(dir, { recursive: true });
   writeJsonAtomic(getLastUsedPath(projectRoot), state);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/credentials.ts` around lines 102 - 105, Remove the redundant explicit
directory creation: delete the mkdirSync(dir, { recursive: true }) call in
src/credentials.ts (the one around line 104) because writeJsonAtomic/atomicWrite
already invokes mkdirSync(dirname(path), { recursive: true }) internally; ensure
you keep the writeJsonAtomic/atomicWrite invocation and do not add any other
directory-creation logic.

211-212: 💤 Low value

Consider removing the redundant ensureParent call.

writeTextAtomicatomicWrite already calls mkdirSync(dirname(path), { recursive: true }) internally, so the explicit ensureParent(textPath) on line 211 is now redundant.

♻️ Proposed simplification
-  const textPath = endpointTextPathFor(projectRoot);
-  ensureParent(textPath);
-  writeTextAtomic(textPath, `http://${endpoint.host}:${endpoint.port}\n`);
+  writeTextAtomic(endpointTextPathFor(projectRoot), `http://${endpoint.host}:${endpoint.port}\n`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/credentials.ts` around lines 211 - 212, Remove the redundant
ensureParent(textPath) call before performing the atomic write: writeTextAtomic
(which delegates to atomicWrite) already creates parent directories via
mkdirSync(dirname(path), { recursive: true }), so delete the ensureParent
invocation and leave the atomic write call (writeTextAtomic/atomicWrite) as the
sole directory-creating mechanism to avoid duplicate work.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/multiplexer/service-state-snapshot.ts`:
- Around line 77-83: When JSON.parse of the saved state at statePath fails in
the try/catch that assigns to existing (SavedState), call
quarantineCorruptFile(statePath) inside the catch before setting existing = null
so the corrupt state.json is preserved for debugging; update the catch block in
the read/parse logic that uses existsSync(statePath) / readFileSync to invoke
quarantineCorruptFile(statePath) and then fall back to existing = null.

---

Nitpick comments:
In `@src/config.ts`:
- Around line 243-249: In saveGlobalConfig, remove the redundant directory
existence check and mkdirSync (the block using getGlobalAimuxDir and
existsSync/mkdirSync) and simply call writeJsonAtomic(getGlobalConfigPath(),
config); rely on writeJsonAtomic to ensure the parent directory exists; update
saveGlobalConfig to only compute getGlobalConfigPath() and call writeJsonAtomic
to simplify the function (references: saveGlobalConfig, getGlobalAimuxDir,
getGlobalConfigPath, writeJsonAtomic).
- Around line 234-240: The saveConfig function currently creates the local dir
before calling writeJsonAtomic, which is redundant because
writeJsonAtomic/atomicWrite already ensures the parent directory exists; remove
the unused const dir = getLocalAimuxDir() and the if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true }); } block and leave only
writeJsonAtomic(getConfigPath(), config) inside saveConfig (refer to saveConfig,
getLocalAimuxDir, writeJsonAtomic, getConfigPath).

In `@src/credentials.ts`:
- Around line 102-105: Remove the redundant explicit directory creation: delete
the mkdirSync(dir, { recursive: true }) call in src/credentials.ts (the one
around line 104) because writeJsonAtomic/atomicWrite already invokes
mkdirSync(dirname(path), { recursive: true }) internally; ensure you keep the
writeJsonAtomic/atomicWrite invocation and do not add any other
directory-creation logic.
- Around line 211-212: Remove the redundant ensureParent(textPath) call before
performing the atomic write: writeTextAtomic (which delegates to atomicWrite)
already creates parent directories via mkdirSync(dirname(path), { recursive:
true }), so delete the ensureParent invocation and leave the atomic write call
(writeTextAtomic/atomicWrite) as the sole directory-creating mechanism to avoid
duplicate work.

In `@src/paths.ts`:
- Around line 413-417: The saveRegistry function currently does an explicit
existsSync/mkdirSync on getGlobalAimuxDir() before calling writeJsonAtomic;
remove the redundant directory check and creation and rely on writeJsonAtomic
(which already calls mkdirSync(dirname(path), { recursive: true }) internally).
Update saveRegistry to simply call writeJsonAtomic(getProjectsRegistryPath(),
registry) and remove the getGlobalAimuxDir/existsSync/mkdirSync code paths to
keep the function concise.

In `@src/team.ts`:
- Around line 127-133: The saveTeamConfig function currently computes dir via
getLocalAimuxDir() and performs an existsSync/mkdirSync check before calling
writeJsonAtomic; remove the redundant directory check and the unused dir
variable and simplify saveTeamConfig to just call
writeJsonAtomic(getProjectTeamPath(), config) because writeJsonAtomic already
ensures the parent directory exists; keep the function name saveTeamConfig and
calls to getProjectTeamPath and writeJsonAtomic unchanged.
- Around line 138-144: The directory existence check and mkdirSync calls inside
saveGlobalTeamConfig are redundant because writeJsonAtomic already ensures the
parent directory exists; remove the block that calls
existsSync(getGlobalAimuxDir()) and mkdirSync(...), leaving saveGlobalTeamConfig
to simply call writeJsonAtomic(getGlobalTeamPath(), config) (matching the
behavior of saveTeamConfig).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c9bd1193-3c2e-4ded-ac1c-18e5c7eb3cdb

📥 Commits

Reviewing files that changed from the base of the PR and between bfea9e5 and e4b7aa4.

📒 Files selected for processing (18)
  • src/atomic-write.test.ts
  • src/atomic-write.ts
  • src/attachment-store.ts
  • src/config.ts
  • src/credentials.ts
  • src/dashboard/ui-state-store.ts
  • src/last-used.ts
  • src/metadata-server.ts
  • src/metadata-store.ts
  • src/multiplexer/persistence-methods.ts
  • src/multiplexer/runtime-lifecycle-methods.ts
  • src/multiplexer/service-state-snapshot.ts
  • src/multiplexer/services.ts
  • src/paths.ts
  • src/project-takeover.ts
  • src/runtime-core/exchange-store.ts
  • src/runtime-core/topology-store.ts
  • src/team.ts

CodeRabbit flagged service-state-snapshot reading state.json, nulling on
parse failure, then unconditionally overwriting it — silently destroying
the corrupt file. saveState's inline service-merge read had the same
overwrite-on-corrupt shape. Both now quarantine before falling back.

The other state.json readers (persistence-methods, services) keep their
write inside the try, so they bail without overwriting on corruption and
need no quarantine here. Addresses CodeRabbit feedback on PR #99.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@TraderSamwise

Copy link
Copy Markdown
Owner Author

Addressed the outside-diff comment on service-state-snapshot.ts in 8bdd3bb — added quarantineCorruptFile(statePath) before the null-fallback so the corrupt state.json is preserved instead of being overwritten. Also fixed the same overwrite-on-corrupt shape in saveState's inline service-merge read. The other two state.json readers (persistence-methods.removePersistedServicesForWorktree, services.ts) keep their write inside the try, so they bail without overwriting on a parse failure and need no quarantine there — the corrupt file survives for loadStateStatic to quarantine on next load.

@TraderSamwise

Copy link
Copy Markdown
Owner Author

The service-state-snapshot.ts outside-diff comment was already addressed in 8bdd3bb (the catch now calls quarantineCorruptFile(statePath) before the null fallback). It re-surfaces because CodeRabbit can't track outside-diff comments as resolvable threads — no further change needed.

@TraderSamwise
TraderSamwise merged commit 7180be5 into master Jun 7, 2026
3 checks passed
@TraderSamwise
TraderSamwise deleted the feat/atomic-durable-writes branch June 7, 2026 02:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant