fix(memory-core): skip stale dreaming recall sources#71695
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
🔒 Aisle Security AnalysisWe found 3 potential security issue(s) in this PR:
1. 🟠 Path traversal enables arbitrary file existence probing (and potential file read) via short-term recall entry paths
Description
Vulnerable code: const absolutePath = path.resolve(workspaceDir, relativePath);
const stat = await fs.stat(sourcePath);RecommendationConstrain resolved paths to an allowed root (e.g., Example fix: function resolveShortTermSourcePathCandidates(workspaceDir: string, candidatePath: string): string[] {
const normalized = normalizeMemoryPath(candidatePath);
// Reject absolute paths and any path with traversal segments
const posix = path.posix.normalize(normalized);
if (path.posix.isAbsolute(posix) || posix.split("/").includes("..")) {
return [];
}
const allowedRoot = path.resolve(workspaceDir, "memory");
const candidates = [posix.startsWith("memory/") ? posix : path.posix.join("memory", path.posix.basename(posix))];
const resolved: string[] = [];
for (const rel of candidates) {
const abs = path.resolve(workspaceDir, rel);
// Enforce containment within allowedRoot
if (abs === allowedRoot || abs.startsWith(allowedRoot + path.sep)) {
resolved.push(abs);
}
}
return [...new Set(resolved)];
}Also consider validating/sanitizing 2. 🟡 Unbounded concurrent fs.stat calls in dreaming phases can exhaust I/O/file descriptors (DoS)
Description
Because
Vulnerable code: const results = await Promise.all(
params.entries.map(async (entry) => ({
entry,
exists: await shortTermRecallSourceExists({ workspaceDir: params.workspaceDir, entry }),
})),
);The risk is amplified because dreaming can be triggered on RecommendationAdd a concurrency limit (and ideally also a max-entry cap) for filesystem existence checks. Example using import pLimit from "p-limit";
export async function filterLiveShortTermRecallEntries(params: {
workspaceDir: string;
entries: ShortTermRecallEntry[];
}): Promise<ShortTermRecallEntry[]> {
const limit = pLimit(20); // tune based on environment
const results = await Promise.all(
params.entries.map((entry) =>
limit(async () => ({
entry,
exists: await shortTermRecallSourceExists({ workspaceDir: params.workspaceDir, entry }),
})),
),
);
return results.filter((r) => r.exists).map((r) => r.entry);
}Additionally, consider:
3. 🟡 Path traversal & dreaming DoS via unvalidated short-term recall entry paths (fs.stat rethrow)
Description
Problems introduced/expanded by this change:
Vulnerable code: const absolutePath = path.resolve(workspaceDir, relativePath);
...
const stat = await fs.stat(sourcePath);
...
if ((err as NodeJS.ErrnoException).code === "ENOENT") continue;
throw err;RecommendationHarden path handling and make existence checks non-fatal.
Example fix: function safeResolveWithinWorkspace(workspaceDir: string, rel: string): string | null {
const normalized = rel.replaceAll("\\", "/");
if (!normalized || path.posix.isAbsolute(normalized)) return null;
const posixNorm = path.posix.normalize(normalized);
if (posixNorm.startsWith("../") || posixNorm === ".." || posixNorm.includes("/../")) return null;
const abs = path.resolve(workspaceDir, posixNorm);
const ws = path.resolve(workspaceDir) + path.sep;
if (!abs.startsWith(ws)) return null;
return abs;
}
// in shortTermRecallSourceExists
const abs = safeResolveWithinWorkspace(workspaceDir, relativePath);
if (!abs) continue;
try {
const stat = await fs.stat(abs);
if (stat.isFile()) return true;
} catch (err) {
const code = (err as NodeJS.ErrnoException)?.code;
if (code === "ENOENT" || code === "EACCES" || code === "EPERM" || code === "ENOTDIR") continue;
return false; // or log debug and continue
}Also consider validating/sanitizing Analyzed PR: #71695 at commit Last updated on: 2026-04-25T18:21:01Z |
Greptile SummaryThis PR fixes stale session-corpus recall entries surfacing in light and REM dreaming output by adding a Confidence Score: 4/5Safe to merge; only one minor P2 style suggestion (sequential vs parallel stat calls), no correctness issues. All logic changes are correct and consistent with existing patterns. The one finding is a P2 performance suggestion about parallelizing Promise.all in filterLiveShortTermRecallEntries; it does not affect correctness given the small typical entry count. No files require special attention. Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/memory-core/src/short-term-promotion.ts
Line: 901-911
Comment:
**Sequential `fs.stat` calls could be parallelized**
`filterLiveShortTermRecallEntries` checks each entry's file existence one at a time. For workspaces with many recall entries this adds unnecessary latency during dreaming. Using `Promise.all` would fan all the stat calls out concurrently.
```suggestion
export async function filterLiveShortTermRecallEntries(params: {
workspaceDir: string;
entries: ShortTermRecallEntry[];
}): Promise<ShortTermRecallEntry[]> {
const results = await Promise.all(
params.entries.map((entry) =>
shortTermRecallSourceExists({ workspaceDir: params.workspaceDir, entry }).then(
(exists) => ({ entry, exists }),
),
),
);
return results.filter(({ exists }) => exists).map(({ entry }) => entry);
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(memory-core): skip stale dreaming re..." | Re-trigger Greptile |
fabianwilliams
left a comment
There was a problem hiding this comment.
Filtering disappeared source files out of REM/Light dreaming candidates is the right scope — referencing deleted recall entries was polluting phase signals. filterLiveShortTermRecallEntries applied symmetrically across both phases. Test covers live-vs-stale separation cleanly. LGTM.
* fix(memory-core): skip stale dreaming recall sources * fix(memory-core): parallelize live recall filtering
* fix(memory-core): skip stale dreaming recall sources * fix(memory-core): parallelize live recall filtering
* fix(memory-core): skip stale dreaming recall sources * fix(memory-core): parallelize live recall filtering
* fix(memory-core): skip stale dreaming recall sources * fix(memory-core): parallelize live recall filtering
* fix(memory-core): skip stale dreaming recall sources * fix(memory-core): parallelize live recall filtering
* fix(memory-core): skip stale dreaming recall sources * fix(memory-core): parallelize live recall filtering
* fix(memory-core): skip stale dreaming recall sources * fix(memory-core): parallelize live recall filtering
Summary
Refs #70104.
memory/.dreams/session-corpus/*.txtrecall next to a live daily memory note.Scope
This addresses the missing-source stale recall path reproduced on current
main. The original #70104 report also describes older signal-accumulation symptoms, so this PR intentionally does not claim to close the entire issue by itself.Root Cause
Short-term recall entries were accepted based on memory path shape alone. Deep promotion already rehydrated source files before applying candidates, but light and REM dreaming read the recall store directly, so missing session-corpus entries could still appear in REM output and receive phase reinforcement.
Validation
OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test extensions/memory-core/src/dreaming-phases.test.tsOPENCLAW_OXLINT_SKIP_LOCK=1 pnpm check:changed