Skip to content

fix(memory): fix atomicity and recovery - #2327

Merged
yyhhyyyyyy merged 3 commits into
devfrom
fix/memory-atomicity-and-recovery
Sep 18, 2026
Merged

yyhhyyyyyy merged 3 commits into
devfrom
fix/memory-atomicity-and-recovery

Conversation

@yyhhyyyyyy

@yyhhyyyyyy yyhhyyyyyy commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Changes

  • Make persona approval and rollback transactional so a failed activation preserves the previous active persona and revisions.
  • Remove the 102,400-ID coverage verification cap that could trigger repeated reindexing. Verify within the existing lease deadline, with cancellation checks and repairs in batches of at most 512 IDs.
  • Invalidate working memory as each candidate commits, so disabling memory during a later model call cannot leave a stale projection after re-enabling. Preserve cancellation and retry behavior.

No schema or public API changes. Splitting agentMemory.ts remains out of scope.

Validation

  • Format, i18n, lint and typecheck passed.
  • Memory suite: 940 passed.
  • Native suite: 334 passed, 2 VSS-dependent legacy tests skipped.
  • Performance suite: 10 passed. Evaluation suite: 7 passed.

Summary by CodeRabbit

  • Bug Fixes
    • Improved memory coverage verification for large stores, helping repair missing vectors and remove orphaned data without unnecessary full reindexing.
    • Improved cancellation handling during memory updates to prevent partial writes, stale data, and unintended follow-up processing.
    • Persona approvals and rollbacks now apply atomically, preserving the previous state when activation fails.
  • Reliability
    • Background memory maintenance now respects operation time limits and remains responsive during large-scale processing.

@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: a6943481-8a42-4d61-8db2-eb1fa8c79e87

📥 Commits

Reviewing files that changed from the base of the PR and between c618d71 and 703ce95.

📒 Files selected for processing (8)
  • docs/issues/memory-coverage-capacity/spec.md
  • docs/issues/memory-partial-write-cancellation/spec.md
  • src/main/memory/infra/embeddingPipeline.ts
  • src/main/memory/services/personaService.ts
  • src/main/memory/services/writeCoordinator.ts
  • test/main/memory/embeddingPipeline.test.ts
  • test/main/memory/memoryUpdateNative.test.ts
  • test/main/memory/writeCoordinator.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The PR updates vector coverage verification, synchronous memory-write bookkeeping, and persona state transitions. It adds specifications and regression tests for lease expiry, cancellation, partial writes, and transactional persona activation.

Changes

Vector coverage verification

Layer / File(s) Summary
Coverage design and contract
docs/issues/memory-coverage-capacity/spec.md
The specification defines lease-bounded paging, batched repairs, cancellation checks, and boundary validation.
Lease-bounded coverage flow
src/main/memory/infra/embeddingPipeline.ts, test/main/memory/embeddingPipeline.test.ts
Coverage verification pages both stores under the lease, validates operation state, batches repairs, and stops without late readiness after cancellation or lease expiry.

Memory write cancellation

Layer / File(s) Summary
Synchronous batch outcome recording
docs/issues/memory-partial-write-cancellation/spec.md, src/main/memory/services/writeCoordinator.ts, test/main/memory/writeCoordinator.test.ts
A shared outcome helper records mutation and working-memory state during batch processing. Tests cover disable, clear, and dispose during pending extraction decisions.

Persona state transactions

Layer / File(s) Summary
Transactional persona transitions
src/main/memory/services/personaService.ts, test/main/memory/memoryUpdateNative.test.ts
Approval and rollback execute paired persona state changes in transactions. Tests verify rollback on activation failure and successful retry.

Priority: ⬇️ Low

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

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Warmup
  participant CoverageVerifier
  participant SQLite
  participant VectorStore
  Warmup->>CoverageVerifier: start verification with operation fence
  CoverageVerifier->>SQLite: enumerate memory IDs by page
  CoverageVerifier->>VectorStore: enumerate embedded IDs by page
  CoverageVerifier->>VectorStore: requeue missing vectors and delete orphans
  CoverageVerifier-->>Warmup: readiness result
Loading
sequenceDiagram
  participant WriteCoordinator
  participant DecisionProvider
  participant MemoryRepository
  participant CancellationState
  WriteCoordinator->>DecisionProvider: process extraction decision
  DecisionProvider-->>WriteCoordinator: batch outcome
  WriteCoordinator->>MemoryRepository: commit touched outcome
  CancellationState->>WriteCoordinator: disable, clear, or dispose
  WriteCoordinator-->>MemoryRepository: stop later writes
Loading

Merge Risk: ⚪ Minimal · up to 703ce

The updated coverage, cancellation, and persona-transition paths have targeted regression coverage, with no concrete merge-blocking risk identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 6 files. (2 skipped: 2 … 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 pull request's main changes to memory atomicity and recovery. It is concise and specific enough for the changeset.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 6 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@yyhhyyyyyy
yyhhyyyyyy merged commit ebfe83a into dev Sep 18, 2026
12 checks passed

@zerob13 zerob13 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review: fix(memory): fix atomicity and recovery

Verdict: Approve. Three real bugs, three small targeted fixes, each backed by a regression test. No over-engineering, no breaking changes, and the code follows patterns already used elsewhere in src/main/memory.

What this PR fixes

1. Persona approve/rollback could leave an agent with no active persona (personaService.ts)

Approve and rollback each write two rows: mark the current persona superseded, mark the target active. These ran as two separate writes. If the second one failed (or the process died between them), the old persona was already superseded and nothing was active — a state with no obvious recovery path for the user.

Fix: wrap both writes in the existing runInTransaction port. This is the same pattern mergeService, conflictService, and managementService already use, so nothing new is invented here. The SQLite integration test proves a mid-transaction failure rolls both writes back and the active persona survives.

2. Partially-committed extraction batches lost their bookkeeping on cancellation (writeCoordinator.ts)

Before: a candidate could restore an archived claim and commit it to SQLite, then the batch awaited the decision provider. Mutation epoch / working-projection-dirty flags were only recorded after the whole batch returned. If memory was disabled while the provider was pending, that bookkeeping was skipped entirely — and after re-enabling, injection kept using the stale working projection, silently missing the row that was actually committed.

Fix: a new recordBatchOutcome helper records markDomainMutationCommitted + markWorkingMemoryDirty at the moment each candidate commits, before any further provider await. Events, embedding triggers and consolidation still respect the operation fence, so a cancelled batch cannot fire late work. The !resolvedModel guard at the single-remember path avoids double bookkeeping. Direct remembers (no model) keep their old synchronous path.

3. Coverage verification was bounded by the wrong limit, causing a permanent rebuild loop (embeddingPipeline.ts)

Before: store warm-up verified vector coverage using the embedding drain's 200-batch guard (~102,400 IDs at 512 per page). A healthy store above that size could never pass verification: warm-up reported failure and triggered a full vector reindex — but rebuilding doesn't reduce the ID count, so every subsequent warm repeated the same "recovery". Net effect: large healthy stores were stuck in a destructive loop.

Fix: keep the same 512-ID keyset pages, but bound total work by the existing 30s vector store lease deadline instead of the batch count, checking cancellation/epoch/generation between pages and yielding so those checks can actually run. A listing that doesn't finish now just defers the warm — it neither certifies readiness nor triggers a reset. Repair work (requeue missing rows, delete orphan vectors) is batched at 512 IDs with yields. Also drops the duplicate ID arrays/sets the old code built (one shared Set now), which is a straight memory win at scale.

Verification

  • pnpm vitest run test/main/memory/writeCoordinator.test.ts test/main/memory/embeddingPipeline.test.ts test/main/memory/memoryUpdateNative.test.ts — 100/100 pass locally, native SQLite included.
  • pnpm typecheck — clean.
  • Boundary tests at 102,399 / 102,400 / 102,401 IDs demonstrate the old cap is gone, and the 102,401 case (equal counts on both sides but one missing + one orphan) guards against count-only certification.
  • The disable/clear/dispose matrix proves: committed rows survive cancellation, no late events / embedding / provider work after cancel, and the disable→re-enable regression fails on the base implementation per the spec.

Minor notes (non-blocking)

  • When verification doesn't finish, the deferred error reuses the vector-store-unavailable reason. Slightly misleading in diagnostics — "verification unfinished" would read better. Cosmetic only.
  • If verification ever exceeds the 30s lease deadline on a real 100k-row store, the lease marks the store suspect and it drains and reopens. The spec openly notes the tests use port fixtures, not a real-database latency benchmark; keyset listing of 100k IDs should be far under 30s in practice, but this is the thing to watch if slow warm loops at scale ever get reported.
  • Test additions are proportionate: every new case maps to an acceptance criterion in the two specs, no coverage padding detected.

Detailed analysis (per file)

  • personaService.ts — approve (L205–L214) and rollback (L293–L302) now wrap their two setPersonaState calls in runInTransaction. There is no await between the getActivePersona read and the transaction, and the main process is single-threaded, so no TOCTOU window is introduced. runInTransaction is better-sqlite3's native transaction (handles nesting via savepoints).
  • writeCoordinator.ts — bookkeeping moved from two completion sites (batch loop L420, single-remember L1386) into recordBatchOutcome (L803), called at all three commit points: immediate settlement, first apply, retry apply. Candidates that settle as noop (forgotten / concurrent-update) correctly skip bookkeeping. Outcomes recorded via raw outcomesByIndex.set for retry-cap overflow are all noops, so no path loses bookkeeping. On cancellation the extraction finally block skips finalizeCommittedExtraction (fence-gated) while the ingestion cursor stays put — committed rows are picked up by the next drain, matching the stated contract.
  • embeddingPipeline.tsverifyVectorCoverage now enumerates SQLite rows inside the store lease with isCurrent() (fence + read epoch + embedding identity + lease generation) checked between every page; the repository query is ORDER BY id ASC + id > ?, so keyset pagination is sound. Sidecar enumeration subtracts from the shared missingIds set; anything not subtracted is an orphan and gets deleted. Duplicate vectors in the store naturally land in extras (second occurrence fails the delete) and are removed — a nice implicit dedupe. Partial requeue followed by an isCurrent() failure is safe: requeued rows leave the ready set, so re-running verification is idempotent. The removed authoritativeListingTruncated auto-reindex has no remaining references.
  • docs/issues/*/spec.md — follows the existing docs/issues/<slug>/spec.md convention; the two specs accurately describe the problems, designs and validation.

All three commits build on each other cleanly and the branch carries no unrelated changes.

@zhangmo8
zhangmo8 deleted the fix/memory-atomicity-and-recovery branch September 20, 2026 07:31
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