Skip to content

feat(memory): harden writes against partial-write and clobber data loss - #128

Merged
aliasunder merged 2 commits into
mainfrom
claude/vault-bootstrap-setup-m65h79
Jun 14, 2026
Merged

feat(memory): harden writes against partial-write and clobber data loss#128
aliasunder merged 2 commits into
mainfrom
claude/vault-bootstrap-setup-m65h79

Conversation

@aliasunder

@aliasunder aliasunder commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Why

In-app defense-in-depth for the memory-layer data-loss class — partial/truncated writes and silent catastrophic shrinks — so the About Me/ memory layer is safe even for adopters without Obsidian Sync version history (which is the only recovery path today).

What

Four safety fixes plus a tool-description update:

  1. Atomic writes. A new atomicWriteFile helper (stage to a unique temp file, then rename over the target) replaces every note/memory writeFile in vault-filesystem, vault-patcher, and memory-store. rename is atomic on the same filesystem, so the target is never truncated — a concurrent reader (the obsidian-sync container) sees either the old or the new content, never a 0-byte or partial write. Bonus: *.tmp staging files are ignored by the file watcher (.md-only), and the rename hands the watcher a guaranteed-complete file.
  2. Large-shrink guard (memory writes only). updateMemory/deleteMemory refuse a write that would drop an existing file below 50% of its size, above a 200-byte floor (which sits just past the empty memory templates, so files with no real content aren't guarded). Fails loud instead of silently truncating. write_note/patch/replace are intentionally left unguarded — their shrinks (full overwrite, empty-new_text deletion) are legitimate.
  3. Size logging. beforeBytes/afterBytes on every write log line for observability.
  4. Graceful SIGTERM. Drain in-flight requests via server.close() with a 10s force-exit fallback, instead of a hard process.exit(0) that could interrupt a write mid-flight.
  5. Tool descriptions. vault_update_memory / vault_delete_memory gain Errors: sections documenting the guard refusal and how to recover.

Testing

  • npm test — 634 passing (15 new: atomic-write success/cleanup, shrink-guard throw/allow/floor, before/after size logging, SIGTERM drain + force-exit, tool-description errors). Guards mutation-checked (verified each test fails when the guard is neutered).
  • npm run build:server and npm run lint clean.

Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved file operation reliability with atomic writes, preventing incomplete file states.
    • Added protective measure against unintended large-scale memory content reduction.
  • Chores

    • Enhanced diagnostic logging with byte-count metrics for improved observability.
    • Strengthened server shutdown handling with graceful closure and configurable timeout protection.

In-app defense-in-depth for the memory-layer data-loss class (the June 13
About Me clobber), so the memory layer is safe even without Obsidian Sync
version history.

- Atomic writes: new atomicWriteFile (stage to temp + rename) replaces every
  note/memory writeFile in vault-filesystem, vault-patcher, and memory-store.
  The target is never truncated, so a concurrent reader (obsidian-sync) sees
  old or new content, never a 0-byte/partial write.
- Large-shrink guard on memory writes: refuse an update/delete that would drop
  an existing file below 50% of its size (above a 200-byte floor that sits just
  past the empty templates), failing loud instead of silently truncating.
- Size logging: before/after byte counts on every write for observability.
- Graceful SIGTERM: drain in-flight requests via server.close() with a 10s
  force-exit fallback instead of a hard process.exit(0).
- Surface the guard error in the vault_update_memory / vault_delete_memory
  tool descriptions (Errors: sections).

634 tests (15 new), build + lint clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@aliasunder, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 50 minutes and 43 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 33dacc84-ad0b-4e7e-931c-a56700eaab31

📥 Commits

Reviewing files that changed from the base of the PR and between b9d8ea8 and b824044.

📒 Files selected for processing (2)
  • src/vault-mcp/server.ts
  • src/vault-mcp/vault-operations/vault-filesystem.ts
📝 Walkthrough

Walkthrough

Adds an atomicWriteFile helper (write-to-temp then rename) and a guardAgainstShrink function (rejects writes that reduce file size by >50% past a 200-byte floor) to the vault-mcp storage layer. All write operations in memory-store.ts, vault-patcher.ts, and vault-filesystem.ts are migrated to atomic writes and now log beforeBytes/afterBytes. Tool descriptions are updated to document the shrink-guard error. A createShutdownHandler export is added to server.ts to drain connections on SIGTERM with a forced-exit watchdog.

Changes

Atomic writes, shrink guard, and byte-count logging

Layer / File(s) Summary
atomicWriteFile utility
src/vault-mcp/vault-operations/vault-filesystem.ts
Adds rm, rename, and randomUUID imports and exports a new atomicWriteFile helper that stages to a UUID-named temp file, renames to the target path, and best-effort removes the temp on failure.
Shrink guard and memory-store atomic writes
src/vault-mcp/vault-operations/memory-store.ts
Removes writeFile import in favour of atomicWriteFile; introduces SHRINK_FLOOR_BYTES, SHRINK_RATIO, and guardAgainstShrink; applies atomic writes and shrink checks in file creation, section creation, entry update/insert, entry deletion, and bootstrapMemoryDir; extends all log payloads with beforeBytes/afterBytes.
vault-patcher and vault-filesystem caller updates
src/vault-mcp/vault-operations/vault-patcher.ts, src/vault-mcp/vault-operations/vault-filesystem.ts
Switches writePatchedNote, writeNote, and updateProperties from writeFile to atomicWriteFile; readNoteForPatch now computes beforeBytes; writePatchedNote returns afterBytes; all write logs include beforeBytes/afterBytes.
Tool descriptions and full test coverage
src/vault-mcp/tool-definitions.ts, src/vault-mcp/__tests__/tool-definitions.test.ts, src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts, src/vault-mcp/vault-operations/__tests__/memory-store.test.ts
Adds shrink-guard error bullets to VAULT_UPDATE_MEMORY and VAULT_DELETE_MEMORY descriptions; adds parameterized description tests; adds atomicWriteFile unit tests (success, no leftover .tmp, rename-failure cleanup); adds memory-store large-shrink-guard and write-size-logging suites; adds vault-filesystem write-size-logging tests.

Graceful SIGTERM shutdown handler

Layer / File(s) Summary
createShutdownHandler implementation, wiring, and tests
src/vault-mcp/server.ts, src/vault-mcp/__tests__/server.test.ts
Exports createShutdownHandler factory that calls httpServer.close(), exits with code 0 on drain completion, and schedules a forceExitMs-millisecond forced exit(1) watchdog (.unref()); updates startServer to capture httpServer and register the handler; tests cover both drain-complete and timeout-forced-exit paths.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Write caller
  participant atomicWriteFile
  participant guardAgainstShrink
  participant fs as Node fs

  Caller->>guardAgainstShrink: beforeBytes, afterBytes
  alt afterBytes < 50% of beforeBytes AND beforeBytes > SHRINK_FLOOR_BYTES
    guardAgainstShrink-->>Caller: throw "refusing memory write"
  else safe
    guardAgainstShrink-->>Caller: ok
    Caller->>atomicWriteFile: filePath, content
    atomicWriteFile->>fs: writeFile(tmpPath)
    atomicWriteFile->>fs: rename(tmpPath, filePath)
    atomicWriteFile-->>Caller: void
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • aliasunder/vault-cortex#81: Both PRs modify src/vault-mcp/tool-definitions.ts and src/vault-mcp/__tests__/tool-definitions.test.ts for VAULT_UPDATE_MEMORY and VAULT_DELETE_MEMORY documentation and assertions.
  • aliasunder/vault-cortex#85: Both PRs touch memory-store.ts, vault-filesystem.ts, and vault-patcher.ts write/update paths and their related tests.
  • aliasunder/vault-cortex#66: Both PRs update VAULT_UPDATE_MEMORY/VAULT_DELETE_MEMORY tool description text in tool-definitions.ts.

Poem

🐇 A bunny writes notes with atomic care,
No half-shrunk memories left in the air!
SIGTERM? No panic — we drain and depart,
With watchdog timers and logs from the heart.
Each byte counted twice, before and after,
The vault stays intact — and that's cause for laughter! 🌿

🚥 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 'feat(memory): harden writes against partial-write and clobber data loss' directly summarizes the main changes: introducing atomic writes and safeguards to prevent data loss. It accurately reflects the primary focus of the changeset.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/vault-bootstrap-setup-m65h79

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/vault-mcp/server.ts (1)

38-43: ⚡ Quick win

Add an explicit outer return type to the exported shutdown factory.

createShutdownHandler is exported, but its outer return type is inferred. Please annotate it explicitly for API clarity and to align with repository rules.

Suggested change
 export const createShutdownHandler =
   (
     httpServer: { close: (callback: () => void) => void },
     forceExitMs = 10_000,
-  ) =>
+  ): (() => void) =>
   (): void => {

As per coding guidelines, "Provide explicit return types on exported functions and modules."

🤖 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/vault-mcp/server.ts` around lines 38 - 43, The exported function
`createShutdownHandler` is a higher-order function (factory pattern) that lacks
an explicit return type annotation. Add an explicit return type annotation to
`createShutdownHandler` to declare what it returns. Since the function returns
another function that takes no parameters and returns void, annotate the return
type as `() => void` on the outer function signature to provide API clarity and
meet the repository's coding guidelines requiring explicit return types on
exported functions.

Source: Coding guidelines

🤖 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/vault-mcp/vault-operations/vault-filesystem.ts`:
- Around line 45-48: The `rm()` call on line 47 in the catch block can throw an
error that overwrites the original `writeFile` or `rename` error, defeating the
purpose of best-effort cleanup. Wrap the `await rm(tmpPath, { force: true })`
call in a nested try-catch block so that if the cleanup fails, the error is
silently caught and the original `err` is still thrown at the end of the catch
block, preserving the root cause of the write failure.

---

Nitpick comments:
In `@src/vault-mcp/server.ts`:
- Around line 38-43: The exported function `createShutdownHandler` is a
higher-order function (factory pattern) that lacks an explicit return type
annotation. Add an explicit return type annotation to `createShutdownHandler` to
declare what it returns. Since the function returns another function that takes
no parameters and returns void, annotate the return type as `() => void` on the
outer function signature to provide API clarity and meet the repository's coding
guidelines requiring explicit return types on exported functions.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ed7f9c1-84c9-4f29-be4f-7d4fc005f06c

📥 Commits

Reviewing files that changed from the base of the PR and between 3a8bcc8 and b9d8ea8.

📒 Files selected for processing (9)
  • src/vault-mcp/__tests__/server.test.ts
  • src/vault-mcp/__tests__/tool-definitions.test.ts
  • src/vault-mcp/server.ts
  • src/vault-mcp/tool-definitions.ts
  • src/vault-mcp/vault-operations/__tests__/memory-store.test.ts
  • src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts
  • src/vault-mcp/vault-operations/memory-store.ts
  • src/vault-mcp/vault-operations/vault-filesystem.ts
  • src/vault-mcp/vault-operations/vault-patcher.ts

Comment thread src/vault-mcp/vault-operations/vault-filesystem.ts
…tdown factory

Address review feedback on PR #128:
- atomicWriteFile: wrap the best-effort temp rm() in its own try/catch so a
  cleanup failure can't overwrite the original write/rename error.
- createShutdownHandler: add the explicit outer return type required for
  exported functions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants