fix(memory-lancedb): skip processed auto-capture messages safely#72663
Conversation
🔒 Aisle Security AnalysisWe found 5 potential security issue(s) in this PR:
1. 🟡 Auto-capture cursor fallback can reset to index 0 causing repeated embedding work (cost-amplification DoS)
DescriptionThe auto-capture cursor logic can fall back to processing from the beginning of the conversation when the previously processed message fingerprint is no longer present in
This creates a cost-amplification/denial-of-service vector where an attacker (or normal compaction behavior) can repeatedly trigger full-history rescans and repeated embedding calls. Vulnerable code: if (cursor.lastMessageFingerprint && cursor.nextIndex > 0) {
for (let index = messages.length - 1; index >= 0; index--) {
if (messageFingerprint(messages[index]) === cursor.lastMessageFingerprint) {
return index + 1;
}
}
return 0;
}RecommendationAvoid resetting to the beginning of the conversation when the cursor cannot be reconciled. Recommended mitigations (pick one or combine):
Example bounded fallback: function resolveAutoCaptureStartIndex(messages: unknown[], cursor?: AutoCaptureCursor): number {
const RECENT_FALLBACK = 20;
if (!cursor) return Math.max(0, messages.length - RECENT_FALLBACK);
if (cursor.lastMessageFingerprint && cursor.nextIndex > 0) {
for (let i = messages.length - 1; i >= 0; i--) {
if (messageFingerprint(messages[i]) === cursor.lastMessageFingerprint) return i + 1;
}
// fingerprint not found -> only process the most recent messages
return Math.max(0, messages.length - RECENT_FALLBACK);
}
return Math.min(cursor.nextIndex, messages.length);
}Also consider adding a cheap dedupe key (e.g., hash of text) before calling 2. 🟡 Algorithmic CPU/memory DoS via JSON.stringify message fingerprinting in auto-capture cursor logic
DescriptionThe auto-capture cursor logic computes and compares message fingerprints by calling
Vulnerable code: for (let index = messages.length - 1; index >= 0; index--) {
if (messageFingerprint(messages[index]) === cursor.lastMessageFingerprint) {
return index + 1;
}
}
// ...
return JSON.stringify({ role: msgObj.role, content: msgObj.content });RecommendationAvoid serializing full Recommended options (prefer 1):
Example bounded hash approach: import { createHash } from "node:crypto";
function boundedMessageFingerprint(message: unknown): string {
const msgObj = asRecord(message);
const role = String(msgObj?.role ?? "");
const content = msgObj?.content;
const preview = typeof content === "string"
? content.slice(0, 256)
: Array.isArray(content)
? JSON.stringify(content.slice(0, 3)).slice(0, 256)
: "";
return createHash("sha256").update(role + ":" + preview).digest("hex");
}Also consider enforcing maximum 3. 🟡 Sensitive user message content retained in memory via autoCaptureCursors fingerprint
DescriptionThe auto-capture cursor mechanism stores a This increases the exposure window for sensitive data because:
Vulnerable code: return JSON.stringify({ role: msgObj.role, content: msgObj.content });
...
autoCaptureCursors.set(cursorKey, {
nextIndex: index + 1,
lastMessageFingerprint: messageFingerprint(message),
});RecommendationAvoid storing raw message content in cursors. Store a non-reversible digest (or other minimal metadata) instead, and consider adding eviction/TTL as defense-in-depth. Example (hashing canonicalized content): import { createHash } from "node:crypto";
function messageFingerprint(message: unknown): string {
const msgObj = asRecord(message);
if (!msgObj) return `${typeof message}:${String(message)}`;
// Only include minimal, stable fields; avoid retaining full content in memory
const canonical = JSON.stringify({ role: msgObj.role, content: msgObj.content });
return createHash("sha256").update(canonical).digest("hex");
}Additionally:
4. 🟡 Unbounded per-session cursor Map can cause memory exhaustion (autoCaptureCursors)
DescriptionThe memory-lancedb plugin introduces a per-session
Vulnerable code: const autoCaptureCursors = new Map<string, AutoCaptureCursor>();
...
autoCaptureCursors.set(cursorKey, {
nextIndex: index + 1,
lastMessageFingerprint: messageFingerprint(message),
});return JSON.stringify({ role: msgObj.role, content: msgObj.content });This creates a memory-exhaustion/DoS risk in the plugin host process when an attacker (or buggy client) can cause many unique sessions and/or very large messages. RecommendationAdd explicit bounds to in-memory per-session cursor tracking and avoid storing raw message content. Suggested mitigations:
Example (bounded + hashed fingerprint): import { createHash } from "node:crypto";
const MAX_CURSORS = 1000;
const cursorTimestamps = new Map<string, number>();
function messageFingerprintHash(message: unknown): string {
const s = JSON.stringify(message, (_k, v) => (typeof v === "string" && v.length > 1024 ? v.slice(0, 1024) : v));
return createHash("sha256").update(s).digest("hex");
}
function setCursor(key: string, cursor: AutoCaptureCursor) {
if (autoCaptureCursors.size >= MAX_CURSORS && !autoCaptureCursors.has(key)) {
// evict oldest
let oldestKey: string | undefined;
let oldestTs = Infinity;
for (const [k, ts] of cursorTimestamps) {
if (ts < oldestTs) { oldestTs = ts; oldestKey = k; }
}
if (oldestKey) { autoCaptureCursors.delete(oldestKey); cursorTimestamps.delete(oldestKey); }
}
autoCaptureCursors.set(key, cursor);
cursorTimestamps.set(key, Date.now());
}Also consider clearing cursors on additional lifecycle events (e.g., agent errors / gateway stop) to reduce leak risk if 5. 🟡 Cross-session cursor state collision via un-namespaced sessionKey/sessionId in memory-lancedb auto-capture cursor map
DescriptionThe The map key is derived from Impact:
Vulnerable code: // agent_end
const cursorKey = ctx.sessionKey ?? ctx.sessionId;
...
autoCaptureCursors.set(cursorKey, { ... });
// session_end
const cursorKey = ctx.sessionKey ?? event.sessionKey ?? ctx.sessionId ?? event.sessionId;
autoCaptureCursors.delete(cursorKey);Because RecommendationUse a stable, non-user-controlled, uniquely-scoped cursor key. Options (pick one):
const cursorKey = ctx.sessionId; // required by PluginHookSessionContext
const cursorKey = `${ctx.agentId ?? "unknown"}:${ctx.sessionId}`;Also make Analyzed PR: #72663 at commit Last updated on: 2026-04-27T06:53:14Z |
Greptile SummaryThis PR introduces per-session cursor tracking in the memory-lancedb Confidence Score: 4/5Safe to merge; the logic is correct and well-covered by the new tests, with one minor defensive-coding concern. No P0 or P1 issues found. The cursor-advance-on-success / retry-on-failure semantics are implemented correctly via try/finally. The fingerprint-based compaction fallback is sound. The only concern is the proactive deletion of nextCursorKey in session_end, which is harmless but unexplained and could cause extra embedding calls on session transitions with intact histories — a P2 style/performance note. extensions/memory-lancedb/index.ts — specifically the session_end handler's nextCursorKey deletion logic. Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/memory-lancedb/index.ts
Line: 751-754
Comment:
**Proactive deletion of `nextCursorKey` may cause redundant re-embedding**
When a session transitions (e.g. daily compaction, `reason: "new"`), deleting the cursor for `nextCursorKey` forces the incoming session to re-scan all messages from index 0 on its first `agent_end`. In practice `resolveAutoCaptureStartIndex` would already return 0 for a compacted history (fingerprint not found), so this delete is mostly defensive. However, for transitions where the history is *not* compacted and the next session key had pre-built cursor state, this causes extra redundant embedding API calls — all caught by the dedup similarity check, but at unnecessary cost. A clarifying comment here would help future readers understand the intent.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(memory-lancedb): skip processed auto..." | Re-trigger Greptile |
| const nextCursorKey = event.nextSessionKey ?? event.nextSessionId; | ||
| if (nextCursorKey) { | ||
| autoCaptureCursors.delete(nextCursorKey); | ||
| } |
There was a problem hiding this comment.
Proactive deletion of
nextCursorKey may cause redundant re-embedding
When a session transitions (e.g. daily compaction, reason: "new"), deleting the cursor for nextCursorKey forces the incoming session to re-scan all messages from index 0 on its first agent_end. In practice resolveAutoCaptureStartIndex would already return 0 for a compacted history (fingerprint not found), so this delete is mostly defensive. However, for transitions where the history is not compacted and the next session key had pre-built cursor state, this causes extra redundant embedding API calls — all caught by the dedup similarity check, but at unnecessary cost. A clarifying comment here would help future readers understand the intent.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/memory-lancedb/index.ts
Line: 751-754
Comment:
**Proactive deletion of `nextCursorKey` may cause redundant re-embedding**
When a session transitions (e.g. daily compaction, `reason: "new"`), deleting the cursor for `nextCursorKey` forces the incoming session to re-scan all messages from index 0 on its first `agent_end`. In practice `resolveAutoCaptureStartIndex` would already return 0 for a compacted history (fingerprint not found), so this delete is mostly defensive. However, for transitions where the history is *not* compacted and the next session key had pre-built cursor state, this causes extra redundant embedding API calls — all caught by the dedup similarity check, but at unnecessary cost. A clarifying comment here would help future readers understand the intent.
How can I resolve this? If you propose a fix, please make it concise.
Summary
Credit
Thanks @as775116191 for the original implementation in #42083. This ProjectClownfish repair keeps that source PR attribution and addresses the Codex/Greptile review findings before validation.
Validation
ProjectClownfish replacement details: