Skip to content

Commit 7492f9a

Browse files
SunnyShu0925claude
andcommitted
[AI] fix(memory-core): address ClawSweeper P1/P2 review findings for #94537
- P1: preserve existing directory mode across replaceFileAtomic calls by stat'ing the pre-created directory and passing its mode as dirMode. Also add preserveExistingMode to separate/deep report paths. - P1: add recall-store-dangling-ref literal to ShortTermAuditIssue.code union in the public SDK facade so typed plugin consumers see it. - P2: align dangling-entry audit predicate with repair by adding isShortTermMemoryPath filter, preventing non-short-term source:memory entries from inflating the dangling count. - Add regression test for audit/repair predicate alignment. Co-Authored-By: Claude Opus 4.8 <[email protected]>
1 parent c30f737 commit 7492f9a

4 files changed

Lines changed: 57 additions & 1 deletion

File tree

extensions/memory-core/src/dreaming-markdown.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ export async function writeDailyDreamingPhaseBlock(params: {
7575
if (shouldWriteInline(params.storage)) {
7676
inlinePath = resolveDailyMemoryPath(params.workspaceDir, nowMs, params.timezone);
7777
await fs.mkdir(path.dirname(inlinePath), { recursive: true });
78+
// Preserve the existing directory mode so replaceFileAtomic's internal
79+
// chmod(dir, dirMode) does not overwrite workspace-relative permissions.
80+
const inlineDirMode = (await fs.stat(path.dirname(inlinePath))).mode;
7881
const original = await fs.readFile(inlinePath, "utf-8").catch((err: unknown) => {
7982
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
8083
return "";
@@ -96,6 +99,7 @@ export async function writeDailyDreamingPhaseBlock(params: {
9699
content: withTrailingNewline(updated),
97100
mode: 0o600,
98101
preserveExistingMode: true,
102+
dirMode: inlineDirMode,
99103
tempPrefix: "dreaming-inline",
100104
throwOnCleanupError: true,
101105
});
@@ -109,6 +113,9 @@ export async function writeDailyDreamingPhaseBlock(params: {
109113
params.timezone,
110114
);
111115
await fs.mkdir(path.dirname(reportPath), { recursive: true });
116+
// Preserve the directory mode freshly created by fs.mkdir so
117+
// replaceFileAtomic's chmod(dir, dirMode) is a no-op.
118+
const reportDirMode = (await fs.stat(path.dirname(reportPath))).mode;
112119
const report = [
113120
`# ${params.phase === "light" ? "Light Sleep" : "REM Sleep"}`,
114121
"",
@@ -119,6 +126,8 @@ export async function writeDailyDreamingPhaseBlock(params: {
119126
filePath: reportPath,
120127
content: report,
121128
mode: 0o600,
129+
preserveExistingMode: true,
130+
dirMode: reportDirMode,
122131
tempPrefix: "dreaming-report",
123132
throwOnCleanupError: true,
124133
});
@@ -157,10 +166,15 @@ export async function writeDeepDreamingReport(params: {
157166
if (shouldWriteSeparate(params.storage)) {
158167
reportPath = resolveSeparateReportPath(params.workspaceDir, "deep", nowMs, params.timezone);
159168
await fs.mkdir(path.dirname(reportPath), { recursive: true });
169+
// Preserve the directory mode freshly created by fs.mkdir so
170+
// replaceFileAtomic's chmod(dir, dirMode) is a no-op.
171+
const deepDirMode = (await fs.stat(path.dirname(reportPath))).mode;
160172
await replaceFileAtomic({
161173
filePath: reportPath,
162174
content: `# Deep Sleep\n\n${body}\n`,
163175
mode: 0o600,
176+
preserveExistingMode: true,
177+
dirMode: deepDirMode,
164178
tempPrefix: "dreaming-deep",
165179
throwOnCleanupError: true,
166180
});

extensions/memory-core/src/short-term-promotion.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3181,6 +3181,45 @@ describe("short-term promotion", () => {
31813181
});
31823182
});
31833183

3184+
it("does not count non-short-term source:memory entries as dangling (predicate alignment)", async () => {
3185+
await withTempWorkspace(async (workspaceDir) => {
3186+
const memoryDir = path.join(workspaceDir, "memory");
3187+
await fs.mkdir(memoryDir, { recursive: true });
3188+
// A source:memory entry whose path does NOT pass isShortTermMemoryPath
3189+
// (e.g. a dreaming deep-file) should not be reported as a dangling ref
3190+
// even though the file genuinely doesn't exist. Audit and repair must use
3191+
// the same predicate to keep --fix re-audit results consistent.
3192+
await testing.writeRawRecallStore(workspaceDir, {
3193+
version: 1,
3194+
updatedAt: "2026-04-04T00:00:00.000Z",
3195+
entries: {
3196+
nonShortTerm: {
3197+
key: "non-short-term",
3198+
path: "memory/dreaming/deep/2026-04-01.md",
3199+
startLine: 1,
3200+
endLine: 1,
3201+
source: "memory",
3202+
snippet: "Deep dreaming file path — not a short-term path.",
3203+
recallCount: 1,
3204+
totalScore: 0.5,
3205+
maxScore: 0.5,
3206+
firstRecalledAt: "2026-04-01T00:00:00.000Z",
3207+
lastRecalledAt: "2026-04-01T00:00:00.000Z",
3208+
queryHashes: ["nd1"],
3209+
recallDays: ["2026-04-01"],
3210+
conceptTags: [],
3211+
},
3212+
},
3213+
});
3214+
3215+
const audit = await auditShortTermPromotionArtifacts({ workspaceDir });
3216+
3217+
// The non-short-term-path entry must not inflate the dangling count.
3218+
expect(audit.danglingRefCount).toBe(0);
3219+
expect(audit.issues.some((issue) => issue.code === "recall-store-dangling-ref")).toBe(false);
3220+
});
3221+
});
3222+
31843223
it("waits for an active short-term lock before repairing", async () => {
31853224
await withTempWorkspace(async (workspaceDir) => {
31863225
await testing.writeRawRecallStore(workspaceDir, {

extensions/memory-core/src/short-term-promotion.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2616,7 +2616,9 @@ export async function auditShortTermPromotionArtifacts(params: {
26162616

26172617
// Check for dangling references — entries whose source file no longer exists.
26182618
const danglingEntryKeys = Object.keys(store.entries).filter(
2619-
(key) => store.entries[key]?.source === "memory",
2619+
(key) =>
2620+
store.entries[key]?.source === "memory" &&
2621+
isShortTermMemoryPath(store.entries[key]?.path ?? ""),
26202622
);
26212623
danglingRefCount = 0;
26222624
for (const key of danglingEntryKeys) {

src/plugin-sdk/memory-core-engine-runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export type ShortTermAuditIssue = {
6363
| "recall-store-empty"
6464
| "recall-store-invalid"
6565
| "recall-store-over-limit"
66+
| "recall-store-dangling-ref"
6667
| "recall-lock-stale"
6768
| "recall-lock-unreadable"
6869
| "qmd-index-missing"

0 commit comments

Comments
 (0)