Skip to content

fix(memory): add watcher error handling and non-zero default intervalMinutes#65755

Closed
nxmxbbd wants to merge 3 commits into
openclaw:mainfrom
nxmxbbd:fix/memory-watcher-resilience-v2
Closed

fix(memory): add watcher error handling and non-zero default intervalMinutes#65755
nxmxbbd wants to merge 3 commits into
openclaw:mainfrom
nxmxbbd:fix/memory-watcher-resilience-v2

Conversation

@nxmxbbd

@nxmxbbd nxmxbbd commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Problem

The chokidar file watcher used by memory-core can silently stop firing events after long gateway uptime. When this happens, the memory index becomes permanently stale — dirty is never set to true, so syncMemoryFiles is never called, and memory search returns outdated results indefinitely.

Additionally, intervalMinutes defaults to 0 (disabled), so there is no fallback mechanism when the watcher dies.

Root Cause

  1. No error handler on watcher — unhandled watcher errors could crash the process
  2. Interval sync doesn't mark dirty — the interval callback calls sync() but dirty stays false, so syncMemoryFiles is skipped entirely
  3. intervalMinutes defaults to 0 — no periodic fallback exists out of the box
  4. extractMemoryErrorReason drops error codes — only captures err.message, missing structured fields like code: 'SQLITE_READONLY'

Fix

  • Register an error handler on the chokidar watcher (logs warning instead of crashing)
  • Set dirty = true in the interval sync callback before calling sync(), so file changes are detected even when the watcher has died
  • Skip interval sync for session-only configs (no memory files to watch)
  • Default intervalMinutes to 15 so interval sync acts as a reliable fallback
  • Use formatErrorMessage() in extractMemoryErrorReason for consistent error serialization

Tests

  • manager.watcher-config.test.ts: verifies interval sync sets dirty = true when watcher has stopped, and that an error handler is registered
  • memory-search.test.ts: verifies intervalMinutes defaults to 15, respects explicit overrides, and respects explicit 0
  • manager.readonly-recovery.test.ts: updated to expect JSON-serialized error format

Supersedes #42042 (clean rewrite, same fix).

…Minutes

The chokidar file watcher can silently stop firing events after long
gateway uptime, leaving the memory index permanently stale. This change:

- Registers an error handler on the watcher so errors are logged instead
  of crashing the process
- Sets dirty=true in the interval sync callback so syncMemoryFiles
  actually re-reads files even when the watcher has died
- Skips interval sync for session-only configs that don't use memory files
- Defaults intervalMinutes to 15 (was 0/disabled) so interval sync acts
  as a reliable fallback when the watcher fails
- Uses formatErrorMessage() in extractMemoryErrorReason for consistent
  error serialization (captures error codes, not just message)
@greptile-apps

greptile-apps Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes three reliability gaps in the memory-core watcher: registers an error handler on the chokidar watcher to prevent silent crashes, forces dirty = true in the interval callback so syncMemoryFiles isn't skipped when the watcher has died, and raises the default intervalMinutes from 0 to 15 so periodic sync is active out of the box.

  • dirty = true is set unconditionally in the interval callback, but this.dirty = false is only executed inside if (shouldSyncMemory), which requires sources.has(\"memory\"). For session-only configs (sessions but no memory), dirty is never cleared and stays permanently true, causing misleading dirty status and spurious sync-on-search invocations.

Confidence Score: 4/5

Safe to merge after addressing the dirty flag leak in session-only configs; all other changes are correct and well-tested.

One P2 logic issue: dirty = true is set unconditionally in the interval callback but is only cleared on the memory-sync path, leaving it permanently true for session-only configs. No P0/P1 issues. Core fixes (watcher error handler, default intervalMinutes: 15, readonly-recovery serialization) are correct and covered by tests.

extensions/memory-core/src/memory/manager-sync-ops.ts — interval callback in ensureIntervalSync

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/memory-core/src/memory/manager-sync-ops.ts
Line: 629-634

Comment:
**`dirty` permanently stuck `true` for session-only configs**

`dirty` is the memory-files dirty flag — it's only cleared at `this.dirty = false` inside `if (shouldSyncMemory)`, which requires `this.sources.has("memory")`. For a config where `sources` has `"sessions"` but not `"memory"`, the interval sets `dirty = true` on every tick but nothing ever clears it, so `dirty` stays `true` for the lifetime of the manager. Downstream effects: `status()` always reports `dirty: true`, and search-triggered syncs (via `startAsyncSearchSync`) see `dirty: true` and invoke `sync()` on every query, even though those calls are no-ops for memory.

Guard the flag assignment with a sources check:

```suggestion
      if (this.sources.has("memory")) {
        this.dirty = true;
      }
      runDetachedMemorySync(() => this.sync({ reason: "interval" }), "interval");
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (2): Last reviewed commit: "fix: preserve interval sync for sessions..." | Re-trigger Greptile

Comment thread extensions/memory-core/src/memory/manager-sync-ops.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

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: 7d319dec43

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +624 to +627
// Skip interval sync when memory files are not enabled - session-only configs
// don't need periodic fallback since sessions have their own sync triggers
if (!this.sources.has("memory")) {
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Preserve interval sync for sessions-only configurations

The new early return in ensureIntervalSync drops the interval timer whenever sources does not include "memory", so a config that indexes only "sessions" now ignores sync.intervalMinutes entirely. Before this change, interval ticks retried pending session sync work (for example after a transient sync failure or when a session-delta trigger was coalesced behind an in-flight sync); now those retries never happen unless another trigger occurs, which can leave session memory stale indefinitely in low-activity agents.

Useful? React with 👍 / 👎.

- Remove unreachable fallback code in extractMemoryErrorReason after formatErrorMessage
- Remove redundant sources.has('memory') guard in interval callback

Fixes P2 suggestions from Greptile review.
@nxmxbbd

nxmxbbd commented Apr 13, 2026

Copy link
Copy Markdown
Contributor Author

Fixed both P2 suggestions in 5ba3fa7:

✅ Removed dead fallback code in extractMemoryErrorReason after formatErrorMessage
✅ Removed redundant sources.has('memory') guard in interval callback

Both suggestions were valid - formatErrorMessage never returns an empty string for any real error input, and the outer guard ensures sources.has('memory') is always true in the callback.

Address Codex bot feedback: sessions-only configs can benefit from interval
sync retries when session sync fails or is coalesced. The sync logic already
handles sessions correctly via shouldSyncSessionsForReindex.
@nxmxbbd

nxmxbbd commented Apr 13, 2026

Copy link
Copy Markdown
Contributor Author

@chatgpt-codex-connector Fixed in afaf869. You're right that sessions-only configs should retain interval sync for retry purposes. Changed the condition to allow interval sync when either memory files OR sessions are enabled, so session sync failures can be retried via the interval fallback mechanism.

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

To use Codex here, create a Codex account and connect to github.

@nxmxbbd

nxmxbbd commented Apr 13, 2026

Copy link
Copy Markdown
Contributor Author

@greptile-apps Fixed the redundant sources.has('memory') guard in 5ba3fa7 — removed the inner check as suggested since the outer guard already ensures only memory-enabled configs create intervals.

@chatgpt-codex-connector Fixed the sessions-only config issue in afaf869 — changed condition to !this.sources.has('memory') && !this.sources.has('sessions') to preserve interval sync for sessions when memory files are disabled. This maintains retry behavior for session sync failures as noted.

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

To use Codex here, create a Codex account and connect to github.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

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: afaf8692b2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// Mark dirty so runSync actually checks for file changes.
// Without this, interval sync is a no-op when the watcher has silently
// stopped firing events — dirty stays false and syncMemoryFiles is skipped.
this.dirty = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Set dirty only when memory source is enabled

This interval callback marks dirty unconditionally, which regresses session-only configurations (sources: ["sessions"]). In that mode runSync never enters the memory branch that clears dirty, so once the first interval fires the manager stays permanently dirty; then startAsyncSearchSync treats every search as needing a sync and repeatedly schedules no-op sync work, and status output remains inaccurately dirty even when there are no pending session deltas.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling extensions: memory-core Extension: memory-core size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant