feat(health): surface search-index freshness - #1177
Conversation
A search index that stops being persisted is invisible from the outside. Search keeps answering from the in-memory copy, /agentmemory/health keeps saying healthy, and the gap only appears on the next restart — when the server loads whatever snapshot last succeeded. On one install here that snapshot was 20 days old: 4944 docs restored against a corpus of 12801, so 61% of memory had quietly stopped being searchable, and the only trace was a throttled warn line. IndexPersistence now tracks its own outcome — doc and vector counts, last successful save, last error, and how long the index has been dirty without a save landing — and exposes it through getIndexPersistenceStatus(). The health snapshot carries it, so evaluateHealth can say so: notes: index_docs_12400_vec_12400_saved_5m_ago alerts: index_persist_error | index_persist_stale_<n>m (both degrade) Nothing changes for a snapshot that carries no index information, so callers constructing HealthSnapshot themselves are unaffected. Signed-off-by: sbynode-ux <sby.node@gmail.com>
|
@sbynode-ux is attempting to deploy a commit to the rohitg00's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThe change tracks search-index persistence state, adds it to health snapshots, evaluates stale or failed persistence, and adds tests for healthy, stale, failed, unavailable, and dirty states. ChangesIndex persistence health
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant IndexPersistence
participant HealthMonitor
participant evaluateHealth
IndexPersistence->>HealthMonitor: provide persistence status
HealthMonitor->>evaluateHealth: evaluate snapshot
evaluateHealth-->>HealthMonitor: persistence alerts or notes
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/state/index-persistence.ts (1)
28-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove explanatory comments from source files.
src/state/index-persistence.ts#L28-L32: remove the persistence-layer explanation.src/state/index-persistence.ts#L38-L38: remove the field explanation.src/state/index-persistence.ts#L52-L52: remove the health-snapshot explanation.src/state/index-persistence.ts#L60-L60: remove the test-seam explanation.src/types.ts#L214-L214: remove the persistence-status explanation.As per coding guidelines,
src/**/*.tsmust not add comments that explain code. Use clear naming instead.🤖 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/state/index-persistence.ts` around lines 28 - 32, Remove the explanatory comments from src/state/index-persistence.ts at lines 28-32, 38, 52, and 60, and from src/types.ts at line 214. Leave the associated persistence implementation, fields, health snapshot, test seam, and status type unchanged.Source: Coding guidelines
test/index-freshness.test.ts (1)
85-105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExercise the stale transition. Use fake timers, call
persistence.stop()to cancel the 5-second debounce, advance time by60 * 60 * 1000 + 1, and assertgetIndexPersistenceStatus().staleistrue. Restore real timers inafterEach.🤖 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 `@test/index-freshness.test.ts` around lines 85 - 105, Extend the test around IndexPersistence.scheduleSave to use fake timers, call persistence.stop() to cancel the debounce, advance time by 60 * 60 * 1000 + 1, and assert getIndexPersistenceStatus().stale is true. Add an afterEach hook that restores real timers, while preserving the existing clean and immediately-dirty assertions.
🤖 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/state/index-persistence.ts`:
- Line 125: Update scheduleSave() and save() to track a monotonically increasing
revision for each scheduled update, capturing the revision when save begins and
clearing dirtySince only when that revision is still current after KV writes
complete. Preserve dirtySince when a newer update was scheduled during the
active save so health continues to report the index as dirty until the latest
save finishes.
---
Nitpick comments:
In `@src/state/index-persistence.ts`:
- Around line 28-32: Remove the explanatory comments from
src/state/index-persistence.ts at lines 28-32, 38, 52, and 60, and from
src/types.ts at line 214. Leave the associated persistence implementation,
fields, health snapshot, test seam, and status type unchanged.
In `@test/index-freshness.test.ts`:
- Around line 85-105: Extend the test around IndexPersistence.scheduleSave to
use fake timers, call persistence.stop() to cancel the debounce, advance time by
60 * 60 * 1000 + 1, and assert getIndexPersistenceStatus().stale is true. Add an
afterEach hook that restores real timers, while preserving the existing clean
and immediately-dirty assertions.
🪄 Autofix
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: 7f7cc4e0-e4b4-450d-93de-1d421890ff3a
📒 Files selected for processing (5)
src/health/monitor.tssrc/health/thresholds.tssrc/state/index-persistence.tssrc/types.tstest/index-freshness.test.ts
| ) {} | ||
|
|
||
| scheduleSave(): void { | ||
| if (status.dirtySince === null) status.dirtySince = Date.now(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Keep dirty state for updates scheduled during an active save.
If scheduleSave() runs while an earlier save() awaits KV writes, the earlier save clears dirtySince at Line 150. The newer update can still be unsaved. Health then reports a clean index until the newer save completes.
Track a revision for each scheduled update. Clear dirtySince only if no later revision was scheduled.
Proposed fix
let status: IndexPersistenceStatus = {
// ...
};
+let dirtyRevision = 0;
export function resetIndexPersistenceStatus(): void {
status = {
// ...
};
+ dirtyRevision = 0;
}
scheduleSave(): void {
if (status.dirtySince === null) status.dirtySince = Date.now();
+ dirtyRevision += 1;
// ...
}
async save(): Promise<void> {
+ const saveRevision = dirtyRevision;
// ...
try {
// ...
status.lastSavedAt = new Date().toISOString();
status.lastError = null;
- status.dirtySince = null;
+ if (dirtyRevision === saveRevision) status.dirtySince = null;Also applies to: 146-150
🤖 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/state/index-persistence.ts` at line 125, Update scheduleSave() and save()
to track a monotonically increasing revision for each scheduled update,
capturing the revision when save begins and clearing dirtySince only when that
revision is still current after KV writes complete. Preserve dirtySince when a
newer update was scheduled during the active save so health continues to report
the index as dirty until the latest save finishes.
Problem
When index persistence stops working, nothing says so.
/agentmemory/healthkeeps returninghealthywarnline thatFAILURE_LOG_THROTTLE_MSemits at most once a minuteOn an install here,
state::sethad been timing out for 20 days. Every restart reloaded a snapshot of 4944 docs against a real corpus of 12 801 — 61% of memory silently unsearchable — and the install reported itself healthy the whole time. I only found it by comparing a file mtime againstkv.listcounts.(That specific timeout is what the sharded manifest work in 0.9.25 addresses. This PR is about the class of failure being visible, whatever the cause: a shard write that fails, a KV that goes read-only, a disk that fills.)
Change
IndexPersistencetracks its own outcome and exposes it:scheduleSave()marks dirty, a successfulsave()clears it and records counts, a failed one records the error. The health snapshot carries the status andevaluateHealthreports it:index_docs_12400_vec_12400_saved_5m_agoindex_persist_errorindex_persist_stale_<n>mA snapshot without
indexPersistenceproduces exactly today's output, so anything constructingHealthSnapshotitself is unaffected.Verification
test/index-freshness.test.ts(new, 5 cases): the note with counts and save age; degrade +index_persist_stale_past the window; degrade +index_persist_erroron a failed save; silence when the snapshot carries no index info; and the dirty-tracking transition onscheduleSave().Full suite green (1601 passed, 1 skipped),
npm run buildclean. The three strict-modetscerrors touching these files are pre-existing onmain.Run the suite with an isolated
HOME— a real~/.agentmemory/.envon the machine leaks into 14 unrelated tests.Summary by CodeRabbit
New Features
Tests