Skip to content

fix(storage): harden #558 satellite backup writes and zst restore - #571

Merged
Wibias merged 2 commits into
lidge-jun:devfrom
Wibias:fix/558-satellite-backup-atomic-zst
Jul 27, 2026
Merged

fix(storage): harden #558 satellite backup writes and zst restore#571
Wibias merged 2 commits into
lidge-jun:devfrom
Wibias:fix/558-satellite-backup-atomic-zst

Conversation

@Wibias

@Wibias Wibias commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up fixes for already-merged PR #558 (Phase 2.1 quarantine restore).

Bugs

  1. Non-atomic satellite-backup.json writeswriteSatelliteBackup used plain writeFileSync, so a crash during the post-memories rewrite (after a satellite DB commit) could truncate or destroy the last valid backup and leave recovery impossible.
  2. Legacy compressed-only quarantine restore failed — manifests whose only rollout file is .jsonl.zst (no plain sibling, no satellite-backup.json thread snapshot) could not reconstruct production-shaped threads rows because restore resolved the logical .jsonl path and refused to parse zstd.

Implementation

  1. Crash-safe satellite backup writes

    • Write to a private temp file in the same stage directory
    • fsync the temp, atomically rename over satellite-backup.json
    • Best-effort directory fsync where supported
    • Applies to the initial backup and every later post-commit update
    • failSatelliteBackupReplace test hook fails after durable temp write / before rename
  2. Legacy .jsonl.zst restore

    • Prefer an existing physical path when the logical rolloutPath is missing
    • readThreadFieldsFromRollout decompresses .zst in memory with zstdDecompressSync({ maxOutputLength }) (64 MiB cap)
    • Never writes an unbounded decompressed copy to disk

Also: test-only /api/storage/trash/restore/test-stream now returns JSON 404 when the cleanup test-hooks env is off, instead of falling through to the GUI SPA (200 HTML) — keeps the #558 responsive-suite gate deterministic.

Test plan

  • Regression: fail post-memories backup replace → prior backup remains parseable and restores every deleted satellite row
  • Regression: sparse legacy manifest + only .jsonl.zst → restore recovers compressed file and correct thread row
  • Focused storage suite + bun run typecheck
  • CI green on Linux / Windows / macOS

Summary by CodeRabbit

  • Bug Fixes
    • Improved restoration of archived sessions stored only in compressed history (.jsonl.zst), including legacy quarantine scenarios.
    • Preserved valid satellite-backup snapshots when cleanup encounters a failure.
  • Reliability
    • Added safeguards for decompressed history size.
    • Improved durability and atomicity of satellite backup replacement to prevent partial/corrupt writes.
  • Tests
    • Added coverage for satellite cleanup/restore failure handling and compressed-only legacy restore behavior.
    • Strengthened assertions for the absent test-stream endpoint response format.

…store

Make every satellite-backup.json update crash-safe (tmp + fsync + rename +
best-effort dir fsync) so a failed post-memories rewrite cannot truncate the
last valid snapshot. Restore legacy compressed-only quarantine entries by
decompressing a lone .jsonl.zst in memory and reconstructing the thread row.
@github-actions github-actions Bot added the bug Something isn't working label Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4fb4c878-6742-44d1-9be3-ec7e2f95d56a

📥 Commits

Reviewing files that changed from the base of the PR and between b11dede and ba53cc0.

📒 Files selected for processing (2)
  • src/storage/cleanup.ts
  • tests/storage-restore-job-responsive.test.ts

📝 Walkthrough

Walkthrough

Archived cleanup now atomically replaces satellite backups with crash-safety hooks, while legacy restore reconstructs thread metadata from compressed .jsonl.zst rollouts. The restore test endpoint’s control flow and fallback response are also covered by expanded tests.

Changes

Archived cleanup and restore

Layer / File(s) Summary
Atomic satellite backup replacement
src/storage/cleanup.ts, tests/storage-cleanup.test.ts
Backups are written, synced, atomically renamed, and directory-synced; replacement failure injection is wired through cleanup hooks and tested during restore failures.
Compressed rollout thread reconstruction
src/codex/history-provider.ts, src/storage/cleanup.ts, tests/storage-cleanup.test.ts
Thread reconstruction supports bounded .jsonl.zst decompression and fallback to existing restored physical paths when manifest rollout resolution is unavailable.
Restore test-stream endpoint guard
src/server/management/logs-usage-routes.ts, tests/storage-restore-job-responsive.test.ts
The test-only route checks its environment flag before request matching and validates the JSON not_available fallback response.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: ingwannu, lidge-jun

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: hardening satellite backup writes and adding zst restore support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b11dedeadf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/storage/cleanup.ts Outdated
const payload = JSON.stringify(backup);
const fd = openSync(tmp, "w", 0o600);
try {
writeSync(fd, payload, null, "utf8");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry short writes before replacing the backup

When the filesystem returns a short write, which can occur for a large backup under disk or resource pressure, this ignores the byte count returned by writeSync, fsyncs the incomplete temp file, and renames it over the last valid satellite-backup.json. The resulting truncated JSON defeats the crash-recovery guarantee this change is intended to provide; loop until the entire payload is written, as appendRolloutLine already does, before fsync and rename.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in ba53cc0: write loop drains short writes before fsync, and rename uses renameAtomicFile (Windows EBUSY/EPERM/EACCES retries).

Comment thread src/storage/cleanup.ts Outdated
throw new Error("test_fail_satellite_backup_replace");
}
try {
renameSync(tmp, dest);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry transient Windows failures during backup replacement

On Windows, antivirus or another scanner can briefly hold the existing backup and make this replacement fail with EBUSY, EPERM, or EACCES; the code then aborts an otherwise valid cleanup after committing satellite mutations and runs the recovery path. The repository already provides renameAtomicFile, including bounded retries for exactly these Windows sharing violations, so use that helper or equivalent retry logic for this atomic replacement.

AGENTS.md reference: AGENTS.md:L36-L37

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in ba53cc0 via renameAtomicFile for the satellite-backup rename.

@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.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@src/server/management/logs-usage-routes.ts`:
- Around line 365-371: Add focused regression tests for the guarded test-stream
route in the routing test suite. Verify GET
/api/storage/trash/restore/test-stream returns a JSON 404 when
OPENCODEX_CLEANUP_TEST_HOOKS is unset or disabled and when
getRestoreTrashTestStreamResponse() returns null, ensuring it does not fall
through to GUI HTML; also verify the enabled hook returns the stream response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8075390b-fa0d-42ea-b850-d0cf9003e9ab

📥 Commits

Reviewing files that changed from the base of the PR and between 40fdc3d and b11dede.

📒 Files selected for processing (4)
  • src/codex/history-provider.ts
  • src/server/management/logs-usage-routes.ts
  • src/storage/cleanup.ts
  • tests/storage-cleanup.test.ts

Comment on lines +365 to 371
if (url.pathname === "/api/storage/trash/restore/test-stream" && req.method === "GET") {
if (process.env.OPENCODEX_CLEANUP_TEST_HOOKS === "1") {
const stream = getRestoreTrashTestStreamResponse();
if (stream) return stream;
}
// Always answer this test-only path — never fall through to the GUI SPA (200 HTML).
return jsonResponse({ error: "not_available" }, 404);

@coderabbitai coderabbitai Bot Jul 27, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add focused regression coverage for the guarded test-stream route.

This change must verify that GET /api/storage/trash/restore/test-stream returns JSON 404—not the GUI HTML—when OPENCODEX_CLEANUP_TEST_HOOKS is unset/disabled and when getRestoreTrashTestStreamResponse() returns null. Also cover the enabled-stream path so future routing changes cannot bypass the test hook.

🤖 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/server/management/logs-usage-routes.ts` around lines 365 - 371, Add
focused regression tests for the guarded test-stream route in the routing test
suite. Verify GET /api/storage/trash/restore/test-stream returns a JSON 404 when
OPENCODEX_CLEANUP_TEST_HOOKS is unset or disabled and when
getRestoreTrashTestStreamResponse() returns null, ensuring it does not fall
through to GUI HTML; also verify the enabled hook returns the stream response.

Source: Path instructions

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

CodeRabbit coverage follow-up: env-unset JSON 404 was already in #571; remaining null-hook + enabled-stream cases are in a new PR targeting dev (test-stream gate regressions).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Wibias, acknowledged. The env-unset JSON 404 regression is already covered in #571, including the JSON response behavior at src/server/management/logs-usage-routes.ts Lines 365-371.

The two remaining branches—hook enabled with getRestoreTrashTestStreamResponse() returning null, and hook enabled with a stream response—are appropriately tracked in the dedicated dev-targeting follow-up PR. I’ll leave this finding unresolved on #571 rather than treating deferred coverage as completed here.

…follow-up

Loop short writes to completion and rename via renameAtomicFile so Windows
sharing violations cannot replace a durable temp with a truncated backup.
@Wibias

Wibias commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@Wibias
Wibias merged commit c573bd9 into lidge-jun:dev Jul 27, 2026
11 checks passed
Wibias added a commit that referenced this pull request Jul 27, 2026
* test(storage): cover restore test-stream route gate cases

Address CodeRabbit on #571: assert JSON 404 when the stream hook is null
under OPENCODEX_CLEANUP_TEST_HOOKS=1, and that the enabled stream path returns
the text/plain chunked response.

* test(storage): isolate restore test-stream route env and hooks

Reset OPENCODEX_CLEANUP_TEST_HOOKS and restore-job hooks in a nested finally
so enableTestStream cannot leak across cases in the responsive suite.
@Wibias
Wibias deleted the fix/558-satellite-backup-atomic-zst branch July 28, 2026 00:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant