Conversation
OpenClaw heartbeat fires agent_end every 30min, which triggered expensive triage + distillation even when no new messages existed. Three fixes: 1. appendOrCreateThread() returns messagesAdded; agent_end handler skips triage/distillation when messagesAdded === 0 (zero-delta guard) 2. Per-thread in-memory capture cooldown prevents burst captures 3. New captureMinInterval config (default 300s) makes cooldown tunable Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds a new numeric configuration option Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant CaptureHook as CaptureHook
participant ThreadStore as ThreadStore
participant TriageService as TriageService
Client->>CaptureHook: agent_end event
CaptureHook->>ThreadStore: read thread._lastCaptureAt
alt within cooldown (now < captureMinInterval)
CaptureHook->>ThreadStore: appendOrCreateThread(event)
ThreadStore-->>CaptureHook: { threadId, normalized, messagesAdded }
alt messagesAdded == 0 or content too short
CaptureHook-->>Client: skip triage/distill
else
CaptureHook->>TriageService: triage/distill(normalized)
TriageService-->>CaptureHook: triage result
CaptureHook->>ThreadStore: update _lastCaptureAt and persist
end
else allowed (no cooldown)
CaptureHook->>ThreadStore: appendOrCreateThread(event)
ThreadStore-->>CaptureHook: { threadId, normalized, messagesAdded }
alt messagesAdded == 0 or content too short
CaptureHook-->>Client: skip triage/distill
else
CaptureHook->>TriageService: triage/distill(normalized)
TriageService-->>CaptureHook: triage result
CaptureHook->>ThreadStore: update _lastCaptureAt and persist
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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 |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@nowledge-mem-openclaw-plugin/src/config.js`:
- Around line 52-56: The captureMinInterval value currently only clamps a lower
bound; update the parsing logic for captureMinInterval (in the config handling
where captureMinInterval is computed) to also enforce the schema's upper bound
by applying Math.min(86400, ...) around the truncated value similar to how
maxRecallResults uses Math.min/Math.max; ensure you still use Math.trunc and
Math.max(0, ...) but wrap that result with Math.min(86400, ...) so values above
86400 are capped to the manifest limit.
In `@nowledge-mem-openclaw-plugin/src/hooks/capture.js`:
- Line 264: The cooldown calculation incorrectly treats an explicit 0 as falsy;
in buildAgentEndCaptureHandler replace the fallback logic so cooldownMs is
derived directly from cfg.captureMinInterval (e.g., remove the "|| 300"
fallback) — i.e., set cooldownMs = cfg.captureMinInterval * 1000 so a configured
0 yields 0ms and the existing if (cooldownMs > 0) guard works as intended;
reference symbols: cfg.captureMinInterval, buildAgentEndCaptureHandler,
cooldownMs.
- Around line 271-280: The cooldown early-return is dropping legitimate
messages; remove the cooldown gate that returns before calling
appendOrCreateThread (the block using cooldownMs, buildStableThreadId, and
_lastCaptureAt) and instead apply the cooldown only around the expensive
triage/distill path after appendOrCreateThread and after you inspect
messagesAdded; call appendOrCreateThread unconditionally so dedup/zero-delta
logic (messagesAdded === 0) can skip work, then check cooldown and update
_lastCaptureAt before running triage/distill so only that expensive step is
rate-limited.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
nowledge-mem-openclaw-plugin/openclaw.plugin.jsonnowledge-mem-openclaw-plugin/src/config.jsnowledge-mem-openclaw-plugin/src/hooks/capture.js
… thread append 1. config.js: add Math.min(86400, ...) to match manifest maximum 2. capture.js: use ?? instead of || so captureMinInterval=0 disables cooldown 3. capture.js: move cooldown to gate only triage/distill path — thread append always runs so legitimate messages are never silently dropped Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
nowledge-mem-openclaw-plugin/src/hooks/capture.js (1)
272-284:⚠️ Potential issue | 🟠 MajorGate
agent_endthread append whenautoCaptureis disabled.At Line 272,
appendOrCreateThreadruns before theautoCaptureguard at Line 283, so thread writes still occur when capture is disabled. This conflicts with the file-level capture policy.🔧 Proposed fix
return async (event, ctx) => { if (!event?.success) return; + if (!cfg.autoCapture) return; // 1. Always thread-append (idempotent, self-guards on empty messages). // Never skip this — messages must always be persisted regardless of // cooldown state, since appendOrCreateThread is deduped and cheap. const result = await appendOrCreateThread({ client, logger, event, ctx, reason: "agent_end", }); - // 2. Triage + distill: language-agnostic LLM-based capture. - // Defensive guard — registration in index.js already gates on autoCapture, - // but check here too so the handler is safe if called directly. - if (!cfg.autoCapture) return; + // 2. Triage + distill: language-agnostic LLM-based capture.As per coding guidelines: "agent_end hook must quality-gate memory note and thread append only when autoCapture is enabled".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@nowledge-mem-openclaw-plugin/src/hooks/capture.js` around lines 272 - 284, The appendOrCreateThread call (result = await appendOrCreateThread({...})) is executed before checking cfg.autoCapture, causing thread writes even when autoCapture is false; update the agent_end handler so you first check if cfg.autoCapture is truthy and only then call appendOrCreateThread (or skip and return early), i.e., move or guard the appendOrCreateThread invocation behind the existing if (!cfg.autoCapture) return; using the same parameters (client, logger, event, ctx, reason: "agent_end") to ensure thread append and subsequent triage/distill run only when autoCapture is enabled.
🧹 Nitpick comments (1)
nowledge-mem-openclaw-plugin/src/hooks/capture.js (1)
9-12: Bound_lastCaptureAtgrowth to avoid long-lived memory creep.The cooldown map currently has no eviction path. On long-running agents with many thread IDs, this can grow indefinitely.
♻️ Suggested refactor
const _lastCaptureAt = new Map(); +const MAX_TRACKED_CAPTURE_THREADS = 10_000;_lastCaptureAt.set(result.threadId, now); + if (_lastCaptureAt.size > MAX_TRACKED_CAPTURE_THREADS) { + const oldestKey = _lastCaptureAt.keys().next().value; + if (oldestKey) _lastCaptureAt.delete(oldestKey); + }Also applies to: 302-303
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@nowledge-mem-openclaw-plugin/src/hooks/capture.js` around lines 9 - 12, The Map _lastCaptureAt in capture.js can grow unbounded; update the capture logic that reads/writes _lastCaptureAt to evict stale entries by age (e.g., remove entries older than a configured TTL like 1 hour) or cap its size with an LRU eviction policy; implement the eviction inside the functions that call/modify _lastCaptureAt (the per-thread capture cooldown helpers) and/or run a lightweight periodic cleanup (setInterval) to prune old threadId keys so long-lived agents do not leak memory.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@nowledge-mem-openclaw-plugin/src/hooks/capture.js`:
- Around line 291-303: The code is recording cooldown too early by calling
_lastCaptureAt.set(result.threadId, Date.now()) before the short-conversation
filters run, which can wrongly suppress the first eligible triage; update the
logic in the capture flow so the cooldown timestamp for result.threadId is only
set after all triage eligibility checks (including the short-conversation
filters around captureMinInterval) pass and just before/after you perform the
triage/distillation action, i.e., remove or move the _lastCaptureAt.set call out
of the early cooldown block and into the code path that actually executes triage
so that you only record a capture when triage runs.
---
Outside diff comments:
In `@nowledge-mem-openclaw-plugin/src/hooks/capture.js`:
- Around line 272-284: The appendOrCreateThread call (result = await
appendOrCreateThread({...})) is executed before checking cfg.autoCapture,
causing thread writes even when autoCapture is false; update the agent_end
handler so you first check if cfg.autoCapture is truthy and only then call
appendOrCreateThread (or skip and return early), i.e., move or guard the
appendOrCreateThread invocation behind the existing if (!cfg.autoCapture)
return; using the same parameters (client, logger, event, ctx, reason:
"agent_end") to ensure thread append and subsequent triage/distill run only when
autoCapture is enabled.
---
Nitpick comments:
In `@nowledge-mem-openclaw-plugin/src/hooks/capture.js`:
- Around line 9-12: The Map _lastCaptureAt in capture.js can grow unbounded;
update the capture logic that reads/writes _lastCaptureAt to evict stale entries
by age (e.g., remove entries older than a configured TTL like 1 hour) or cap its
size with an LRU eviction policy; implement the eviction inside the functions
that call/modify _lastCaptureAt (the per-thread capture cooldown helpers) and/or
run a lightweight periodic cleanup (setInterval) to prune old threadId keys so
long-lived agents do not leak memory.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
nowledge-mem-openclaw-plugin/src/config.jsnowledge-mem-openclaw-plugin/src/hooks/capture.js
🚧 Files skipped from review as they are similar to previous changes (1)
- nowledge-mem-openclaw-plugin/src/config.js
Cooldown was set before short-conversation filters, so if those filters skipped triage, the cooldown was burned without LLM work actually running. Move _lastCaptureAt.set() to after all eligibility checks pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
nowledge-mem-openclaw-plugin/src/hooks/capture.js (1)
269-283:⚠️ Potential issue | 🟠 MajorGate
appendOrCreateThreadbehindcfg.autoCaptureinagent_end.
appendOrCreateThread(...)executes before theif (!cfg.autoCapture) returnguard, so direct invocation of this handler can still persist messages while auto-capture is disabled.🔧 Proposed fix
return async (event, ctx) => { if (!event?.success) return; + if (!cfg.autoCapture) return; // 1. Always thread-append (idempotent, self-guards on empty messages). // Never skip this — messages must always be persisted regardless of // cooldown state, since appendOrCreateThread is deduped and cheap. const result = await appendOrCreateThread({ client, logger, event, ctx, reason: "agent_end", }); // 2. Triage + distill: language-agnostic LLM-based capture. // Defensive guard — registration in index.js already gates on autoCapture, // but check here too so the handler is safe if called directly. - if (!cfg.autoCapture) return;As per coding guidelines
nowledge-mem-openclaw-plugin/src/hooks/capture.js: “agent_end hook must quality-gate memory note and thread append only when autoCapture is enabled”.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@nowledge-mem-openclaw-plugin/src/hooks/capture.js` around lines 269 - 283, The call to appendOrCreateThread in the agent_end handler runs unconditionally and can persist messages even when cfg.autoCapture is false; wrap or move that call so it only executes when cfg.autoCapture is true (i.e., check cfg.autoCapture before invoking appendOrCreateThread or place the appendOrCreateThread block inside the existing if (!cfg.autoCapture) guard), ensuring all logic that appends or creates threads is gated by cfg.autoCapture while preserving any other unconditional behaviour in agent_end.
🧹 Nitpick comments (1)
nowledge-mem-openclaw-plugin/src/hooks/capture.js (1)
9-11: Bound_lastCaptureAtgrowth to avoid long-lived memory drift.
_lastCaptureAtis process-global and never evicted. Over many sessions, stale thread keys can accumulate indefinitely. Consider opportunistic TTL cleanup (or capped LRU) when setting cooldown entries.Also applies to: 315-320
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@nowledge-mem-openclaw-plugin/src/hooks/capture.js` around lines 9 - 11, _current code creates a process-global Map named _lastCaptureAt that never evicts keys; modify the capture logic where _lastCaptureAt is written and read (the code paths that set/check per-thread cooldown) to perform opportunistic cleanup: when setting an entry, prune keys older than a configurable TTL (e.g., compare stored timestamp to Date.now() - TTL) and/or enforce a max size by removing least-recently-used keys; ensure the TTL and max-size constants are declared near _lastCaptureAt and that reads also ignore/cleanup expired entries so stale thread IDs do not accumulate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@nowledge-mem-openclaw-plugin/src/hooks/capture.js`:
- Around line 269-283: The call to appendOrCreateThread in the agent_end handler
runs unconditionally and can persist messages even when cfg.autoCapture is
false; wrap or move that call so it only executes when cfg.autoCapture is true
(i.e., check cfg.autoCapture before invoking appendOrCreateThread or place the
appendOrCreateThread block inside the existing if (!cfg.autoCapture) guard),
ensuring all logic that appends or creates threads is gated by cfg.autoCapture
while preserving any other unconditional behaviour in agent_end.
---
Nitpick comments:
In `@nowledge-mem-openclaw-plugin/src/hooks/capture.js`:
- Around line 9-11: _current code creates a process-global Map named
_lastCaptureAt that never evicts keys; modify the capture logic where
_lastCaptureAt is written and read (the code paths that set/check per-thread
cooldown) to perform opportunistic cleanup: when setting an entry, prune keys
older than a configurable TTL (e.g., compare stored timestamp to Date.now() -
TTL) and/or enforce a max size by removing least-recently-used keys; ensure the
TTL and max-size constants are declared near _lastCaptureAt and that reads also
ignore/cleanup expired entries so stale thread IDs do not accumulate.
Sweep entries older than 24h when map exceeds 200 entries to prevent unbounded growth in long-lived processes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OpenClaw heartbeat fires agent_end every 30min, which triggered expensive triage + distillation even when no new messages existed. Three fixes:
Summary by CodeRabbit
New Features
Improvements
Chores