fix(memory): preserve retry state and embedding cache across reindex rollback#59137
fix(memory): preserve retry state and embedding cache across reindex rollback#59137TSHOGX wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1b05d16d12
ℹ️ 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".
Greptile SummaryThis PR addresses two related gaps in the Key changes:
The implementation is coherent and the invariants are correctly maintained across both the atomic (temp-DB swap) and the unsafe (test-fast) reindex paths. Confidence Score: 5/5Safe to merge — no data-loss or correctness regressions found; all changes are in the rollback/retry path with no impact on the successful reindex flow. After tracing the full code paths — including confirming that the targeted post-compaction sync returns early at line 1114 via No files require special attention. Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/memory-core/src/memory/manager-sync-ops.ts
Line: 692
Comment:
**`Math.min` for `lastSize` may over-penalise on reset**
Using `Math.min(snapshotState.lastSize, liveState.lastSize)` is intentionally conservative: if the failed reindex had already advanced `lastSize` to the new file size but the temp DB was never committed, restoring to the snapshot value prevents the next sync from skipping bytes that were only indexed in the rolled-back DB.
In practice this works correctly because `resetSessionDelta` always sets `lastSize` to `entry.size` (never below), so `live.lastSize` is either equal to or larger than `snapshot.lastSize` in the normal failure case. The one edge-case worth noting: if session delta tracking is ever reset to zero before the failure (e.g. from a future `resetIndex` that clears `sessionDeltas`), `Math.min(snapshot, 0) = 0` would force a full re-read of every session file on the next sync — correct, but potentially surprising. A brief inline note alongside the existing comment would help future readers understand this is intentional rather than a conservative shortcut.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(memory): keep full session retries p..." | Re-trigger Greptile |
|
Thanks for the context here. I did a careful shell check against current Current main now carries the useful retry-state and embedding-cache recovery through the merged replacement PR, while this older source branch is conflicting and its remaining differences are obsolete branch shape and release-changelog churn. So I’m closing this older PR as already covered on Review detailsBest possible solution: Keep the merged replacement implementation on main and close this conflicting source branch rather than trying to merge or rebase it. Do we have a high-confidence way to reproduce the issue? Yes. The source PR includes real CLI/SQLite A/B terminal proof for interrupted force reindex recovery, and current main now has focused regression tests for rollback retry state and cache mirroring. Is this the best way to solve the issue? Yes. The merged replacement is the better solution path because it ports the useful recovery behavior onto current main, preserves the temp-DB cleanup ordering, and avoids the source branch's obsolete release-changelog edit. Security review: Security review cleared: No concrete security or supply-chain concern remains; the source PR and merged replacement are memory-core SQLite retry/cache logic and tests without new dependencies, workflows, permissions, scripts, or secret handling. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against db5e415888ae; fix evidence: commit e99ab385cfe1, main fix timestamp 2026-06-14T15:31:51+08:00. |
6808525 to
0ed55aa
Compare
|
Ported onto current Re: clawsweeper "needs changes before merge"
What landed
Acceptance
Diff is now 8 files, +749/−62, entirely within |
…rollback - Snapshot sync state (dirty flags, session dirty files, session deltas, plus a new sessionFullRetryPending sentinel) before a full reindex and restore the union on rollback, so the next sync retries the rolled-back work instead of silently converging. - During a safe temp-DB reindex, mirror successful per-batch embedding cache writes back to the original index so the cache survives a later batch failure (the temp DB is thrown away on rollback). - seedEmbeddingCache now checks for the embedding_cache table on the source DB so enabling cache on an older index no longer crashes the reindex. - Run unsafe reindex inside the same snapshot/restore wrapper without restoring stale vector dims (the live DB has been mutated in place). - Focused helper unit tests and a manager-level test that force a second-batch failure and assert cache + retry state survive. Ports PR openclaw#59137 onto the current memory-core helper layout.
…rollback - Snapshot sync state (dirty flags, session dirty files, session deltas, plus a new sessionFullRetryPending sentinel) before a full reindex and restore the union on rollback, so the next sync retries the rolled-back work instead of silently converging. - During a safe temp-DB reindex, mirror successful per-batch embedding cache writes back to the original index so the cache survives a later batch failure (the temp DB is thrown away on rollback). - seedEmbeddingCache now checks for the embedding_cache table on the source DB so enabling cache on an older index no longer crashes the reindex. - Run unsafe reindex inside the same snapshot/restore wrapper without restoring stale vector dims (the live DB has been mutated in place). - Focused helper unit tests and a manager-level test that force a second-batch failure and assert cache + retry state survive. Ports PR openclaw#59137 onto the current memory-core helper layout.
0ed55aa to
b4315b7
Compare
|
I took a pass at the clawsweeper P2 cleanup finding and prepared a narrow follow-up commit here: Commit: cb02215 fix(memory): close temp db before reindex cleanup What it changes:
Local verification:
I do not have push access to the PR head branch, so this is available for cherry-pick or for maintainer review if useful. |
|
ClawSweeper PR egg 🔥 Warming up: real-behavior proof passed; findings, security review, or rank-up moves are still in progress. Hatch commandComment Hatchability rules:
What is this egg doing here?
|
|
Following up on the remaining ClawSweeper cleanup blocker. I prepared a narrow follow-up branch for the Commit: It keeps this PR's retry-state restoration and embedding-cache mirror behavior unchanged, and only restores the current Local verification from that branch: The PR still shows |
|
This pull request has been automatically marked as stale due to inactivity. |
|
Clownfish 🐠 reef update Thanks for the work here. Clownfish could not write to the source branch, so it opened a replacement PR rather than letting the fix drift. attribution still points back here. Replacement PR: #92881 fish notes: model gpt-5.5, reasoning xhigh; reviewed against a5c93b0. |
|
ClawSweeper applied the proposed close for this PR.
|
Summary
memory index --forceshould degrade to "retry the same work next sync" and should not waste the embedding batches that already succeeded before the failure. Today they do both.sessionFullRetryPendingsentinel so a failed full session rebuild still forces the next sync onto the full path even ifsessionsDirtyFileswas cleared; during a safe reindex, mirror every successful embedding cache batch back to the original DB so the cache survives a later-batch rollback; and haveseedEmbeddingCachecheck for theembedding_cachetable on the source DB so enabling cache on an older index no longer crashes the reindex.AI-assisted: yes. I used Claude Opus 4.7 to port the original fix onto the current
manager-sync-ops/manager-embedding-opslayout and reviewed the diff and validation output.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Real behavior proof (required for external PRs)
Embedding cacheat0on retry, throwing away the embedding work the provider had already returned before the failure.pnpm+ OpenClaw checkout; two isolated$OPENCLAW_HOMEdirs with identical starting DB (14408 chunks · 0 cache rows) and identical workspace (45 local markdown files). Remote provider: real SiliconFlowopenai/BAAI/bge-m3,memorySearch.cache.enabled: true,cap = 500000. Git worktrees atupstream/mainandfix/memory-reindex-recovery.sqlite3 $OPENCLAW_HOME/.openclaw/memory/main.sqlite 'DELETE FROM embedding_cache;'(baseline confirmed: 0 rows).OPENCLAW_HOME=... pnpm openclaw memory index --force(against real provider).OPENCLAW_HOME=... pnpm openclaw memory status.sqlite3 $OPENCLAW_HOME/.openclaw/memory/main.sqlite 'SELECT COUNT(*) FROM embedding_cache;'.home-fixrun —Memory index failed (main): fetch failed, thenopenclaw memory statusprintsEmbedding cache: enabled (109 entries), andsqlite3 ... 'SELECT COUNT(*) FROM embedding_cache'prints109. Row breakdownSELECT provider, model, COUNT(*) ...returnsopenai|BAAI/bge-m3|109withMIN/MAX(updated_at)both landing inside the interrupted reindex window (2026-05-09 10:01:34..10:01:36). Full A/B terminal transcript is pasted under## Evidencebelow.upstream/mainthe same offline force-reindex leavesembedding_cacheat0rows; onfix/memory-reindex-recoverythe same run preserves109rows written through the mirror write, which survive the temp-DB rollback. The committedchunks/filestable counts are unchanged on both sides (2384 files · 14408 chunks), confirming the atomic swap is still atomic.sessionFullRetryPendingstate machine (covered by focused manager-level tests instead); multi-process concurrent indexing; running the full pre-fix flow against a remote provider that rate-limits instead of failing DNS.upstream/main(64514a6) the same offline force-reindex emitsMemory index failed (main): getaddrinfo ENOTFOUND api.siliconflow.cn, thenopenclaw memory statusprintsEmbedding cache: enabled (0 entries)andsqlite3 ... 'SELECT COUNT(*) FROM embedding_cache'prints0— captured in this same evidence window.Root Cause (if applicable)
runSafeReindex/runUnsafeReindexpreserved storage/vector state on rollback but did not snapshot or restore the in-memory sync state (dirty,sessionsDirty,sessionsDirtyFiles,sessionDeltas). A failed full session rebuild could clear the per-file dirty filter mid-run and leave no sentinel for the next sync to see, soshouldSyncSessionsForReindexfell back tosessionsDirty && dirtySessionFileCount > 0and silently downgraded to an incremental sync. Separately,embedChunksInBatchesaccumulated cache entries until all batches finished and wrote them only to the temp DB, so a later-batch failure threw away every successful batch along with the temp DB.extensions/memory-core/src/memory/*with the helper-split layout, the old reliability work in fix(memory): harden builtin sync and reindex semantics #55497 no longer applied cleanly. This PR replays the narrow fix on top of the current layout.Regression Test Plan (if applicable)
extensions/memory-core/src/memory/manager-sync-recovery.test.ts(new)extensions/memory-core/src/memory/manager-session-reindex.test.ts(new)extensions/memory-core/src/memory/manager.reindex-recovery.test.ts(new)snapshotSyncState/computeRestoredSyncStateunion sticky dirty flags and dirty file sets;mergeSessionDeltastakes the laterlastSizeand the conservativependingBytes/pendingMessages;restoreRetryStateAfterReindexRollbackflips the sentinel only when the failed attempt actually began a full session rebuild.shouldSyncSessionsForReindexforces a full sync whensessionFullRetryPendingis set, even with zero dirty files.sync({ force: true })succeeds, and enabling cache on an index that was built without theembedding_cachetable no longer crashes.manager.atomic-reindex.test.tscovered "previous index survives failed temp reindex" but not retry state or cache preservation.User-visible / Behavior Changes
sessionFullRetryPendingso the next normal sync still performs a full session retry instead of silently downgrading to an incremental pass.embedding_cachetable existed can enable cache without failing reindex.Diagram (if applicable)
Security Impact (required)
No)No)No)No)No)Yes, explain risk + mitigation: N/A.Repro + Verification
Environment
provider: openai,model: BAAI/bge-m3) for manual repro; mocked provider in targeted testsmemorySearch.provider: "openai"memorySearch.model: "BAAI/bge-m3"memorySearch.cache.enabled: trueSteps
$OPENCLAW_HOME.sqlite3 $OPENCLAW_HOME/.openclaw/memory/main.sqlite 'DELETE FROM embedding_cache;'OPENCLAW_HOME=... pnpm openclaw memory index --force(real SiliconFlow provider).main) or cancel once retry backoff has spent the retry budget (onfix, see note below).OPENCLAW_HOME=... pnpm openclaw memory statusandsqlite3 ... 'SELECT COUNT(*) FROM embedding_cache;'Note on fix-side cancellation: the retry classifier treats
fetch failed-class wrapped errors as retryable, so per-item fallback after a failed batch keeps retrying for a while; on main the error surfaced as rawgetaddrinfo ENOTFOUNDand exited on its own. Early-cancelling the fix run after the retries are all exhausted does not affect the0vs109evidence — those rows were already mirrored to the original DB before cancellation. Tightening that retry classifier for obviously-permanent network failures is out of scope here (see "Known follow-ups" below).Expected
main:Embedding cachecount stays at 0 (successful batches are in the temp DB that gets thrown away).Embedding cachecount > 0 (mirrored to the original DB).Actual
upstream/main(64514a6):Memory index failed (main): getaddrinfo ENOTFOUND api.siliconflow.cn;memory status→Embedding cache: enabled (0 entries);SELECT COUNT(*)→ 0.fix/memory-reindex-recovery-v2(0ed55aa):memory status→Embedding cache: enabled (109 entries);SELECT COUNT(*)→ 109, all rowsprovider=openai / model=BAAI/bge-m3,updated_atinside the reindex window.Evidence
Attach at least one:
Validation run:
Manual CLI A/B (same starting DB and workspace in both isolated homes, real SiliconFlow provider, Wi-Fi turned off mid-reindex):
Human Verification (required)
pnpm test extensions/memory-core— 56 files / 631 tests green, including the three new suites for recovery helpers,shouldSyncSessionsForReindex, and the manager-level rollback integration.upstream/mainbefore this PR.embedding_cacherows after rollback.embedding_cachefrom an existing index and reopened with cache enabled — reindex completes and the table is re-created.BAAI/bge-m3), Wi-Fi cut mid-reindex:maincache stays at 0 after rollback, this branch preserves 109 rows (provider+model+timestamp all from the interrupted reindex window).sessionFullRetryPending(it only clears on a full rebuild success, not on targeted refresh).mergeSessionDeltaswhen one side haspendingBytes: 0and the other does not — the conservativeminstill does not silently wipe pending work.sessionFullRetryPendingstate machine (covered by focused manager-level tests).Known follow-ups (out of scope for this PR)
isRetryableMemoryEmbeddingErrorclassifies anyfetch failed-wrapped error as retryable, including permanent failures likegetaddrinfo ENOTFOUND. Combined with per-item fallback after a failed batch, an offline reindex on some Node versions keeps looping for a long time before the process exits — surfaces as an apparent hang. Pre-existing onmain, unrelated to rollback recovery. A separate narrow PR can addENOTFOUNDto the non-retryable set or put a total-elapsed budget on the retry loop.Review Conversations
Compatibility / Migration
Yes)No)No)Risks and Mitigations
sessionFullRetryPendingadds one bit to the session sync state machine.shouldSyncSessionsForReindexis the single point of consumption, unit-tested in isolation; targeted session refresh preserves the flag and only full-rebuild success clears it.embedding_cacherows only (reusable work), never the searchable index rows; the committed searchable index swap remains atomic; regression coverage locks in the intended rollback behavior.embedding_cache.seedEmbeddingCacheprobes for the table insqlite_masterbefore reading or mirroring; regression test exercises enabling cache on a pre-cache index.