@gethmy/mcp 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +30 -5
- package/dist/index.js +30 -5
- package/package.json +1 -1
- package/src/__tests__/memory-audit.test.ts +90 -0
- package/src/memory-audit.ts +51 -2
- package/src/server.ts +19 -3
package/dist/cli.js
CHANGED
|
@@ -28087,7 +28087,7 @@ function isBoilerplate(title, content) {
|
|
|
28087
28087
|
}
|
|
28088
28088
|
return false;
|
|
28089
28089
|
}
|
|
28090
|
-
function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
|
|
28090
|
+
function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraftAgeDays) {
|
|
28091
28091
|
const now = Date.now();
|
|
28092
28092
|
const ageDays = (now - Date.parse(entity.created_at)) / MS_PER_DAY;
|
|
28093
28093
|
const effectiveLastAccess = entity.last_accessed_at ?? entity.created_at;
|
|
@@ -28172,6 +28172,7 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
|
|
|
28172
28172
|
bucket = "review";
|
|
28173
28173
|
else
|
|
28174
28174
|
bucket = "keep";
|
|
28175
|
+
const staleDraft = entity.memory_tier === "draft" && (entity.access_count || 0) === 0 && ageDays > staleDraftAgeDays;
|
|
28175
28176
|
return {
|
|
28176
28177
|
id: entity.id,
|
|
28177
28178
|
title: entity.title,
|
|
@@ -28183,6 +28184,7 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
|
|
|
28183
28184
|
reasons,
|
|
28184
28185
|
legacy,
|
|
28185
28186
|
legacyReasons,
|
|
28187
|
+
staleDraft,
|
|
28186
28188
|
subScores: {
|
|
28187
28189
|
confidence: Math.round(confidence),
|
|
28188
28190
|
decay: Math.round(decay),
|
|
@@ -28198,6 +28200,7 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
|
|
|
28198
28200
|
const archiveBelow = options?.archiveBelow ?? 40;
|
|
28199
28201
|
const deleteBelow = options?.deleteBelow ?? 20;
|
|
28200
28202
|
const limit = options?.limit ?? 500;
|
|
28203
|
+
const staleDraftAgeDays = options?.staleDraftAgeDays ?? 7;
|
|
28201
28204
|
const report = {
|
|
28202
28205
|
success: true,
|
|
28203
28206
|
dryRun,
|
|
@@ -28210,7 +28213,8 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
|
|
|
28210
28213
|
review: 0,
|
|
28211
28214
|
archive: 0,
|
|
28212
28215
|
delete: 0,
|
|
28213
|
-
legacyCount: 0
|
|
28216
|
+
legacyCount: 0,
|
|
28217
|
+
staleDraftCount: 0
|
|
28214
28218
|
},
|
|
28215
28219
|
actionsTaken: { flaggedReview: 0, archived: 0, deleted: 0 },
|
|
28216
28220
|
distribution: { "0-20": 0, "20-40": 0, "40-70": 0, "70-100": 0 },
|
|
@@ -28221,6 +28225,7 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
|
|
|
28221
28225
|
noGraphPresence: 0
|
|
28222
28226
|
},
|
|
28223
28227
|
lowest: [],
|
|
28228
|
+
staleDrafts: [],
|
|
28224
28229
|
errors: [],
|
|
28225
28230
|
healthReport: ""
|
|
28226
28231
|
};
|
|
@@ -28270,12 +28275,14 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
|
|
|
28270
28275
|
const audits = [];
|
|
28271
28276
|
for (const entity of entities) {
|
|
28272
28277
|
const relCount = relationCounts.get(entity.id) ?? 0;
|
|
28273
|
-
const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow);
|
|
28278
|
+
const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow, staleDraftAgeDays);
|
|
28274
28279
|
audits.push(audit);
|
|
28275
28280
|
report.summary.scanned++;
|
|
28276
28281
|
report.summary[audit.bucket]++;
|
|
28277
28282
|
if (audit.legacy)
|
|
28278
28283
|
report.summary.legacyCount++;
|
|
28284
|
+
if (audit.staleDraft)
|
|
28285
|
+
report.summary.staleDraftCount++;
|
|
28279
28286
|
if (audit.score < 20)
|
|
28280
28287
|
report.distribution["0-20"]++;
|
|
28281
28288
|
else if (audit.score < 40)
|
|
@@ -28296,6 +28303,7 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
|
|
|
28296
28303
|
}
|
|
28297
28304
|
}
|
|
28298
28305
|
report.lowest = [...audits].sort((a, b) => a.score - b.score).slice(0, 10);
|
|
28306
|
+
report.staleDrafts = audits.filter((a) => a.staleDraft).sort((a, b) => b.ageDays - a.ageDays);
|
|
28299
28307
|
if (!dryRun) {
|
|
28300
28308
|
for (const audit of audits) {
|
|
28301
28309
|
try {
|
|
@@ -28341,7 +28349,7 @@ function renderReport(report) {
|
|
|
28341
28349
|
const lines = [
|
|
28342
28350
|
`# Memory Quality Audit
|
|
28343
28351
|
`,
|
|
28344
|
-
`**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount}`,
|
|
28352
|
+
`**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount} | **Stale drafts:** ${s.staleDraftCount}`,
|
|
28345
28353
|
"",
|
|
28346
28354
|
"## Distribution",
|
|
28347
28355
|
`- 70-100 (keep): ${report.distribution["70-100"]}`,
|
|
@@ -28365,6 +28373,17 @@ function renderReport(report) {
|
|
|
28365
28373
|
lines.push(`- No tags + no relations: ${l.noGraphPresence}`);
|
|
28366
28374
|
lines.push("");
|
|
28367
28375
|
}
|
|
28376
|
+
if (report.staleDrafts.length > 0) {
|
|
28377
|
+
lines.push("## Stale Drafts (promote-or-drop candidates)");
|
|
28378
|
+
lines.push("Filter: `tier=draft AND access=0 AND age>threshold`");
|
|
28379
|
+
lines.push("| Age | Score | Title |");
|
|
28380
|
+
lines.push("|-----|-------|-------|");
|
|
28381
|
+
for (const a of report.staleDrafts.slice(0, 20)) {
|
|
28382
|
+
const titleTrunc = a.title.length > 50 ? `${a.title.slice(0, 47)}...` : a.title;
|
|
28383
|
+
lines.push(`| ${a.ageDays}d | ${a.score} | ${titleTrunc} |`);
|
|
28384
|
+
}
|
|
28385
|
+
lines.push("");
|
|
28386
|
+
}
|
|
28368
28387
|
if (report.lowest.length > 0) {
|
|
28369
28388
|
lines.push("## Lowest-Scoring (top 10)");
|
|
28370
28389
|
lines.push("| Score | Bucket | Tier | Age | Title | Reasons |");
|
|
@@ -30459,6 +30478,10 @@ var TOOLS = {
|
|
|
30459
30478
|
limit: {
|
|
30460
30479
|
type: "number",
|
|
30461
30480
|
description: "Max number of entities to audit (default: 500). Paginated fetch."
|
|
30481
|
+
},
|
|
30482
|
+
staleDraftAgeDays: {
|
|
30483
|
+
type: "number",
|
|
30484
|
+
description: "Age threshold (days) for the stale-draft filter: flags drafts with 0 accesses older than this. Reported separately from bucket scoring — surfaces promote-or-drop candidates the thresholds miss. Default: 7."
|
|
30462
30485
|
}
|
|
30463
30486
|
},
|
|
30464
30487
|
required: []
|
|
@@ -32038,7 +32061,8 @@ async function handleToolCall(name, args, deps) {
|
|
|
32038
32061
|
dryRun: args.dryRun,
|
|
32039
32062
|
archiveBelow: args.archiveBelow,
|
|
32040
32063
|
deleteBelow: args.deleteBelow,
|
|
32041
|
-
limit: args.limit
|
|
32064
|
+
limit: args.limit,
|
|
32065
|
+
staleDraftAgeDays: args.staleDraftAgeDays
|
|
32042
32066
|
});
|
|
32043
32067
|
return {
|
|
32044
32068
|
success: report.success,
|
|
@@ -32048,6 +32072,7 @@ async function handleToolCall(name, args, deps) {
|
|
|
32048
32072
|
legacyBreakdown: report.legacyBreakdown,
|
|
32049
32073
|
actionsTaken: report.actionsTaken,
|
|
32050
32074
|
lowest: report.lowest,
|
|
32075
|
+
staleDrafts: report.staleDrafts,
|
|
32051
32076
|
errors: report.errors,
|
|
32052
32077
|
healthReport: report.healthReport
|
|
32053
32078
|
};
|
package/dist/index.js
CHANGED
|
@@ -25847,7 +25847,7 @@ function isBoilerplate(title, content) {
|
|
|
25847
25847
|
}
|
|
25848
25848
|
return false;
|
|
25849
25849
|
}
|
|
25850
|
-
function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
|
|
25850
|
+
function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraftAgeDays) {
|
|
25851
25851
|
const now = Date.now();
|
|
25852
25852
|
const ageDays = (now - Date.parse(entity.created_at)) / MS_PER_DAY;
|
|
25853
25853
|
const effectiveLastAccess = entity.last_accessed_at ?? entity.created_at;
|
|
@@ -25932,6 +25932,7 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
|
|
|
25932
25932
|
bucket = "review";
|
|
25933
25933
|
else
|
|
25934
25934
|
bucket = "keep";
|
|
25935
|
+
const staleDraft = entity.memory_tier === "draft" && (entity.access_count || 0) === 0 && ageDays > staleDraftAgeDays;
|
|
25935
25936
|
return {
|
|
25936
25937
|
id: entity.id,
|
|
25937
25938
|
title: entity.title,
|
|
@@ -25943,6 +25944,7 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow) {
|
|
|
25943
25944
|
reasons,
|
|
25944
25945
|
legacy,
|
|
25945
25946
|
legacyReasons,
|
|
25947
|
+
staleDraft,
|
|
25946
25948
|
subScores: {
|
|
25947
25949
|
confidence: Math.round(confidence),
|
|
25948
25950
|
decay: Math.round(decay),
|
|
@@ -25958,6 +25960,7 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
|
|
|
25958
25960
|
const archiveBelow = options?.archiveBelow ?? 40;
|
|
25959
25961
|
const deleteBelow = options?.deleteBelow ?? 20;
|
|
25960
25962
|
const limit = options?.limit ?? 500;
|
|
25963
|
+
const staleDraftAgeDays = options?.staleDraftAgeDays ?? 7;
|
|
25961
25964
|
const report = {
|
|
25962
25965
|
success: true,
|
|
25963
25966
|
dryRun,
|
|
@@ -25970,7 +25973,8 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
|
|
|
25970
25973
|
review: 0,
|
|
25971
25974
|
archive: 0,
|
|
25972
25975
|
delete: 0,
|
|
25973
|
-
legacyCount: 0
|
|
25976
|
+
legacyCount: 0,
|
|
25977
|
+
staleDraftCount: 0
|
|
25974
25978
|
},
|
|
25975
25979
|
actionsTaken: { flaggedReview: 0, archived: 0, deleted: 0 },
|
|
25976
25980
|
distribution: { "0-20": 0, "20-40": 0, "40-70": 0, "70-100": 0 },
|
|
@@ -25981,6 +25985,7 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
|
|
|
25981
25985
|
noGraphPresence: 0
|
|
25982
25986
|
},
|
|
25983
25987
|
lowest: [],
|
|
25988
|
+
staleDrafts: [],
|
|
25984
25989
|
errors: [],
|
|
25985
25990
|
healthReport: ""
|
|
25986
25991
|
};
|
|
@@ -26030,12 +26035,14 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
|
|
|
26030
26035
|
const audits = [];
|
|
26031
26036
|
for (const entity of entities) {
|
|
26032
26037
|
const relCount = relationCounts.get(entity.id) ?? 0;
|
|
26033
|
-
const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow);
|
|
26038
|
+
const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow, staleDraftAgeDays);
|
|
26034
26039
|
audits.push(audit);
|
|
26035
26040
|
report.summary.scanned++;
|
|
26036
26041
|
report.summary[audit.bucket]++;
|
|
26037
26042
|
if (audit.legacy)
|
|
26038
26043
|
report.summary.legacyCount++;
|
|
26044
|
+
if (audit.staleDraft)
|
|
26045
|
+
report.summary.staleDraftCount++;
|
|
26039
26046
|
if (audit.score < 20)
|
|
26040
26047
|
report.distribution["0-20"]++;
|
|
26041
26048
|
else if (audit.score < 40)
|
|
@@ -26056,6 +26063,7 @@ async function runMemoryAudit(client3, workspaceId, projectId, options) {
|
|
|
26056
26063
|
}
|
|
26057
26064
|
}
|
|
26058
26065
|
report.lowest = [...audits].sort((a, b) => a.score - b.score).slice(0, 10);
|
|
26066
|
+
report.staleDrafts = audits.filter((a) => a.staleDraft).sort((a, b) => b.ageDays - a.ageDays);
|
|
26059
26067
|
if (!dryRun) {
|
|
26060
26068
|
for (const audit of audits) {
|
|
26061
26069
|
try {
|
|
@@ -26101,7 +26109,7 @@ function renderReport(report) {
|
|
|
26101
26109
|
const lines = [
|
|
26102
26110
|
`# Memory Quality Audit
|
|
26103
26111
|
`,
|
|
26104
|
-
`**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount}`,
|
|
26112
|
+
`**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount} | **Stale drafts:** ${s.staleDraftCount}`,
|
|
26105
26113
|
"",
|
|
26106
26114
|
"## Distribution",
|
|
26107
26115
|
`- 70-100 (keep): ${report.distribution["70-100"]}`,
|
|
@@ -26125,6 +26133,17 @@ function renderReport(report) {
|
|
|
26125
26133
|
lines.push(`- No tags + no relations: ${l.noGraphPresence}`);
|
|
26126
26134
|
lines.push("");
|
|
26127
26135
|
}
|
|
26136
|
+
if (report.staleDrafts.length > 0) {
|
|
26137
|
+
lines.push("## Stale Drafts (promote-or-drop candidates)");
|
|
26138
|
+
lines.push("Filter: `tier=draft AND access=0 AND age>threshold`");
|
|
26139
|
+
lines.push("| Age | Score | Title |");
|
|
26140
|
+
lines.push("|-----|-------|-------|");
|
|
26141
|
+
for (const a of report.staleDrafts.slice(0, 20)) {
|
|
26142
|
+
const titleTrunc = a.title.length > 50 ? `${a.title.slice(0, 47)}...` : a.title;
|
|
26143
|
+
lines.push(`| ${a.ageDays}d | ${a.score} | ${titleTrunc} |`);
|
|
26144
|
+
}
|
|
26145
|
+
lines.push("");
|
|
26146
|
+
}
|
|
26128
26147
|
if (report.lowest.length > 0) {
|
|
26129
26148
|
lines.push("## Lowest-Scoring (top 10)");
|
|
26130
26149
|
lines.push("| Score | Bucket | Tier | Age | Title | Reasons |");
|
|
@@ -28219,6 +28238,10 @@ var TOOLS = {
|
|
|
28219
28238
|
limit: {
|
|
28220
28239
|
type: "number",
|
|
28221
28240
|
description: "Max number of entities to audit (default: 500). Paginated fetch."
|
|
28241
|
+
},
|
|
28242
|
+
staleDraftAgeDays: {
|
|
28243
|
+
type: "number",
|
|
28244
|
+
description: "Age threshold (days) for the stale-draft filter: flags drafts with 0 accesses older than this. Reported separately from bucket scoring — surfaces promote-or-drop candidates the thresholds miss. Default: 7."
|
|
28222
28245
|
}
|
|
28223
28246
|
},
|
|
28224
28247
|
required: []
|
|
@@ -29798,7 +29821,8 @@ async function handleToolCall(name, args, deps) {
|
|
|
29798
29821
|
dryRun: args.dryRun,
|
|
29799
29822
|
archiveBelow: args.archiveBelow,
|
|
29800
29823
|
deleteBelow: args.deleteBelow,
|
|
29801
|
-
limit: args.limit
|
|
29824
|
+
limit: args.limit,
|
|
29825
|
+
staleDraftAgeDays: args.staleDraftAgeDays
|
|
29802
29826
|
});
|
|
29803
29827
|
return {
|
|
29804
29828
|
success: report.success,
|
|
@@ -29808,6 +29832,7 @@ async function handleToolCall(name, args, deps) {
|
|
|
29808
29832
|
legacyBreakdown: report.legacyBreakdown,
|
|
29809
29833
|
actionsTaken: report.actionsTaken,
|
|
29810
29834
|
lowest: report.lowest,
|
|
29835
|
+
staleDrafts: report.staleDrafts,
|
|
29811
29836
|
errors: report.errors,
|
|
29812
29837
|
healthReport: report.healthReport
|
|
29813
29838
|
};
|
package/package.json
CHANGED
|
@@ -282,6 +282,96 @@ describe("runMemoryAudit", () => {
|
|
|
282
282
|
).toBe(0.25);
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
+
test("stale-draft filter flags draft+0access+age>threshold separately from bucket", async () => {
|
|
286
|
+
const { client } = makeMockClient(
|
|
287
|
+
[
|
|
288
|
+
// Stale draft — should be flagged by the filter, but otherwise healthy
|
|
289
|
+
// enough to bucket as "review" (not archive).
|
|
290
|
+
{
|
|
291
|
+
id: "stale-draft",
|
|
292
|
+
type: "context",
|
|
293
|
+
title:
|
|
294
|
+
"Task transition: feature work started but never touched again",
|
|
295
|
+
content:
|
|
296
|
+
"This draft has enough content and tags to score reasonably, " +
|
|
297
|
+
"but nobody ever accessed it after creation — classic promote-or-drop candidate.",
|
|
298
|
+
confidence: 0.4,
|
|
299
|
+
memory_tier: "draft",
|
|
300
|
+
access_count: 0,
|
|
301
|
+
last_accessed_at: null,
|
|
302
|
+
created_at: daysAgo(10),
|
|
303
|
+
tags: ["task"],
|
|
304
|
+
embedding: [0.1],
|
|
305
|
+
},
|
|
306
|
+
// Fresh draft — same shape but under the age threshold, must NOT flag.
|
|
307
|
+
{
|
|
308
|
+
id: "fresh-draft",
|
|
309
|
+
type: "context",
|
|
310
|
+
title: "Task transition: a fresh draft still within the grace window",
|
|
311
|
+
content:
|
|
312
|
+
"Content long enough to not be thin at all, really properly sized.",
|
|
313
|
+
confidence: 0.4,
|
|
314
|
+
memory_tier: "draft",
|
|
315
|
+
access_count: 0,
|
|
316
|
+
last_accessed_at: null,
|
|
317
|
+
created_at: daysAgo(3),
|
|
318
|
+
tags: ["task"],
|
|
319
|
+
embedding: [0.1],
|
|
320
|
+
},
|
|
321
|
+
// Non-draft old zero-access — must NOT flag (filter is draft-only).
|
|
322
|
+
{
|
|
323
|
+
id: "old-episode",
|
|
324
|
+
type: "pattern",
|
|
325
|
+
title: "Episode entity that is old and unaccessed but not a draft",
|
|
326
|
+
content:
|
|
327
|
+
"Sometimes reference/episode tier entities sit unaccessed; " +
|
|
328
|
+
"they're not draft-promotion candidates so the filter should skip them.",
|
|
329
|
+
confidence: 0.8,
|
|
330
|
+
memory_tier: "episode",
|
|
331
|
+
access_count: 0,
|
|
332
|
+
last_accessed_at: null,
|
|
333
|
+
created_at: daysAgo(30),
|
|
334
|
+
tags: ["pat"],
|
|
335
|
+
embedding: [0.1],
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
{ "stale-draft": 1, "fresh-draft": 1, "old-episode": 2 },
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const report = await runMemoryAudit(client, "ws-1");
|
|
342
|
+
expect(report.summary.staleDraftCount).toBe(1);
|
|
343
|
+
expect(report.staleDrafts).toHaveLength(1);
|
|
344
|
+
expect(report.staleDrafts[0].id).toBe("stale-draft");
|
|
345
|
+
expect(report.healthReport).toContain("Stale Drafts");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("staleDraftAgeDays option tunes the filter threshold", async () => {
|
|
349
|
+
const { client } = makeMockClient([
|
|
350
|
+
{
|
|
351
|
+
id: "draft-5d",
|
|
352
|
+
type: "context",
|
|
353
|
+
title: "Five-day-old untouched draft",
|
|
354
|
+
content:
|
|
355
|
+
"Content long enough to pass the thin-content check, absolutely.",
|
|
356
|
+
confidence: 0.4,
|
|
357
|
+
memory_tier: "draft",
|
|
358
|
+
access_count: 0,
|
|
359
|
+
last_accessed_at: null,
|
|
360
|
+
created_at: daysAgo(5),
|
|
361
|
+
tags: ["x"],
|
|
362
|
+
embedding: [0.1],
|
|
363
|
+
},
|
|
364
|
+
]);
|
|
365
|
+
|
|
366
|
+
const defaultRun = await runMemoryAudit(client, "ws-1");
|
|
367
|
+
expect(defaultRun.summary.staleDraftCount).toBe(0);
|
|
368
|
+
|
|
369
|
+
const tightRun = await runMemoryAudit(client, "ws-1", undefined, {
|
|
370
|
+
staleDraftAgeDays: 3,
|
|
371
|
+
});
|
|
372
|
+
expect(tightRun.summary.staleDraftCount).toBe(1);
|
|
373
|
+
});
|
|
374
|
+
|
|
285
375
|
test("fetch error surfaces as report.success=false", async () => {
|
|
286
376
|
const client = {
|
|
287
377
|
listMemoryEntities: mock(async () => {
|
package/src/memory-audit.ts
CHANGED
|
@@ -46,6 +46,14 @@ export interface AuditOptions {
|
|
|
46
46
|
deleteBelow?: number;
|
|
47
47
|
includeLegacyFlag?: boolean;
|
|
48
48
|
limit?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Age threshold (days) for the stale-draft filter. A memory is flagged
|
|
51
|
+
* stale when `tier=draft AND access_count=0 AND age > staleDraftAgeDays`.
|
|
52
|
+
* Stale drafts are reported separately — they don't change scoring or
|
|
53
|
+
* bucketing, just surface promote-or-drop candidates the thresholds miss.
|
|
54
|
+
* Default: 7.
|
|
55
|
+
*/
|
|
56
|
+
staleDraftAgeDays?: number;
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
interface EntityAudit {
|
|
@@ -59,6 +67,7 @@ interface EntityAudit {
|
|
|
59
67
|
reasons: string[];
|
|
60
68
|
legacy: boolean;
|
|
61
69
|
legacyReasons: string[];
|
|
70
|
+
staleDraft: boolean;
|
|
62
71
|
subScores: {
|
|
63
72
|
confidence: number;
|
|
64
73
|
decay: number;
|
|
@@ -82,6 +91,7 @@ export interface AuditReport {
|
|
|
82
91
|
archive: number;
|
|
83
92
|
delete: number;
|
|
84
93
|
legacyCount: number;
|
|
94
|
+
staleDraftCount: number;
|
|
85
95
|
};
|
|
86
96
|
actionsTaken: {
|
|
87
97
|
flaggedReview: number;
|
|
@@ -101,6 +111,7 @@ export interface AuditReport {
|
|
|
101
111
|
noGraphPresence: number;
|
|
102
112
|
};
|
|
103
113
|
lowest: EntityAudit[];
|
|
114
|
+
staleDrafts: EntityAudit[];
|
|
104
115
|
errors: Array<{ entityId?: string; step: string; message: string }>;
|
|
105
116
|
healthReport: string;
|
|
106
117
|
}
|
|
@@ -128,6 +139,7 @@ function scoreEntity(
|
|
|
128
139
|
relationCount: number,
|
|
129
140
|
archiveBelow: number,
|
|
130
141
|
deleteBelow: number,
|
|
142
|
+
staleDraftAgeDays: number,
|
|
131
143
|
): EntityAudit {
|
|
132
144
|
const now = Date.now();
|
|
133
145
|
const ageDays = (now - Date.parse(entity.created_at)) / MS_PER_DAY;
|
|
@@ -232,6 +244,14 @@ function scoreEntity(
|
|
|
232
244
|
else if (score < 70) bucket = "review";
|
|
233
245
|
else bucket = "keep";
|
|
234
246
|
|
|
247
|
+
// Stale-draft filter — orthogonal to bucketing. A draft that's aged past
|
|
248
|
+
// the threshold without a single access is a promote-or-drop candidate
|
|
249
|
+
// regardless of its composite score.
|
|
250
|
+
const staleDraft =
|
|
251
|
+
entity.memory_tier === "draft" &&
|
|
252
|
+
(entity.access_count || 0) === 0 &&
|
|
253
|
+
ageDays > staleDraftAgeDays;
|
|
254
|
+
|
|
235
255
|
return {
|
|
236
256
|
id: entity.id,
|
|
237
257
|
title: entity.title,
|
|
@@ -243,6 +263,7 @@ function scoreEntity(
|
|
|
243
263
|
reasons,
|
|
244
264
|
legacy,
|
|
245
265
|
legacyReasons,
|
|
266
|
+
staleDraft,
|
|
246
267
|
subScores: {
|
|
247
268
|
confidence: Math.round(confidence),
|
|
248
269
|
decay: Math.round(decay),
|
|
@@ -264,6 +285,7 @@ export async function runMemoryAudit(
|
|
|
264
285
|
const archiveBelow = options?.archiveBelow ?? 40;
|
|
265
286
|
const deleteBelow = options?.deleteBelow ?? 20;
|
|
266
287
|
const limit = options?.limit ?? 500;
|
|
288
|
+
const staleDraftAgeDays = options?.staleDraftAgeDays ?? 7;
|
|
267
289
|
|
|
268
290
|
const report: AuditReport = {
|
|
269
291
|
success: true,
|
|
@@ -278,6 +300,7 @@ export async function runMemoryAudit(
|
|
|
278
300
|
archive: 0,
|
|
279
301
|
delete: 0,
|
|
280
302
|
legacyCount: 0,
|
|
303
|
+
staleDraftCount: 0,
|
|
281
304
|
},
|
|
282
305
|
actionsTaken: { flaggedReview: 0, archived: 0, deleted: 0 },
|
|
283
306
|
distribution: { "0-20": 0, "20-40": 0, "40-70": 0, "70-100": 0 },
|
|
@@ -288,6 +311,7 @@ export async function runMemoryAudit(
|
|
|
288
311
|
noGraphPresence: 0,
|
|
289
312
|
},
|
|
290
313
|
lowest: [],
|
|
314
|
+
staleDrafts: [],
|
|
291
315
|
errors: [],
|
|
292
316
|
healthReport: "",
|
|
293
317
|
};
|
|
@@ -345,11 +369,18 @@ export async function runMemoryAudit(
|
|
|
345
369
|
const audits: EntityAudit[] = [];
|
|
346
370
|
for (const entity of entities) {
|
|
347
371
|
const relCount = relationCounts.get(entity.id) ?? 0;
|
|
348
|
-
const audit = scoreEntity(
|
|
372
|
+
const audit = scoreEntity(
|
|
373
|
+
entity,
|
|
374
|
+
relCount,
|
|
375
|
+
archiveBelow,
|
|
376
|
+
deleteBelow,
|
|
377
|
+
staleDraftAgeDays,
|
|
378
|
+
);
|
|
349
379
|
audits.push(audit);
|
|
350
380
|
report.summary.scanned++;
|
|
351
381
|
report.summary[audit.bucket]++;
|
|
352
382
|
if (audit.legacy) report.summary.legacyCount++;
|
|
383
|
+
if (audit.staleDraft) report.summary.staleDraftCount++;
|
|
353
384
|
|
|
354
385
|
// Distribution bin
|
|
355
386
|
if (audit.score < 20) report.distribution["0-20"]++;
|
|
@@ -373,6 +404,11 @@ export async function runMemoryAudit(
|
|
|
373
404
|
// Top 10 lowest-scoring
|
|
374
405
|
report.lowest = [...audits].sort((a, b) => a.score - b.score).slice(0, 10);
|
|
375
406
|
|
|
407
|
+
// All stale drafts — oldest first (most overdue for promote-or-drop)
|
|
408
|
+
report.staleDrafts = audits
|
|
409
|
+
.filter((a) => a.staleDraft)
|
|
410
|
+
.sort((a, b) => b.ageDays - a.ageDays);
|
|
411
|
+
|
|
376
412
|
// Execute actions
|
|
377
413
|
if (!dryRun) {
|
|
378
414
|
for (const audit of audits) {
|
|
@@ -420,7 +456,7 @@ function renderReport(report: AuditReport): string {
|
|
|
420
456
|
const s = report.summary;
|
|
421
457
|
const lines: string[] = [
|
|
422
458
|
"# Memory Quality Audit\n",
|
|
423
|
-
`**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount}`,
|
|
459
|
+
`**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount} | **Stale drafts:** ${s.staleDraftCount}`,
|
|
424
460
|
"",
|
|
425
461
|
"## Distribution",
|
|
426
462
|
`- 70-100 (keep): ${report.distribution["70-100"]}`,
|
|
@@ -446,6 +482,19 @@ function renderReport(report: AuditReport): string {
|
|
|
446
482
|
lines.push("");
|
|
447
483
|
}
|
|
448
484
|
|
|
485
|
+
if (report.staleDrafts.length > 0) {
|
|
486
|
+
lines.push("## Stale Drafts (promote-or-drop candidates)");
|
|
487
|
+
lines.push("Filter: `tier=draft AND access=0 AND age>threshold`");
|
|
488
|
+
lines.push("| Age | Score | Title |");
|
|
489
|
+
lines.push("|-----|-------|-------|");
|
|
490
|
+
for (const a of report.staleDrafts.slice(0, 20)) {
|
|
491
|
+
const titleTrunc =
|
|
492
|
+
a.title.length > 50 ? `${a.title.slice(0, 47)}...` : a.title;
|
|
493
|
+
lines.push(`| ${a.ageDays}d | ${a.score} | ${titleTrunc} |`);
|
|
494
|
+
}
|
|
495
|
+
lines.push("");
|
|
496
|
+
}
|
|
497
|
+
|
|
449
498
|
if (report.lowest.length > 0) {
|
|
450
499
|
lines.push("## Lowest-Scoring (top 10)");
|
|
451
500
|
lines.push("| Score | Bucket | Tier | Age | Title | Reasons |");
|
package/src/server.ts
CHANGED
|
@@ -1709,11 +1709,13 @@ const TOOLS = {
|
|
|
1709
1709
|
},
|
|
1710
1710
|
auditArchiveBelow: {
|
|
1711
1711
|
type: "number",
|
|
1712
|
-
description:
|
|
1712
|
+
description:
|
|
1713
|
+
"Audit: archive entities scoring below this (default: 40)",
|
|
1713
1714
|
},
|
|
1714
1715
|
auditDeleteBelow: {
|
|
1715
1716
|
type: "number",
|
|
1716
|
-
description:
|
|
1717
|
+
description:
|
|
1718
|
+
"Audit: delete entities scoring below this (default: 20)",
|
|
1717
1719
|
},
|
|
1718
1720
|
},
|
|
1719
1721
|
required: [],
|
|
@@ -1753,6 +1755,11 @@ const TOOLS = {
|
|
|
1753
1755
|
description:
|
|
1754
1756
|
"Max number of entities to audit (default: 500). Paginated fetch.",
|
|
1755
1757
|
},
|
|
1758
|
+
staleDraftAgeDays: {
|
|
1759
|
+
type: "number",
|
|
1760
|
+
description:
|
|
1761
|
+
"Age threshold (days) for the stale-draft filter: flags drafts with 0 accesses older than this. Reported separately from bucket scoring — surfaces promote-or-drop candidates the thresholds miss. Default: 7.",
|
|
1762
|
+
},
|
|
1756
1763
|
},
|
|
1757
1764
|
required: [],
|
|
1758
1765
|
},
|
|
@@ -4165,6 +4172,7 @@ async function handleToolCall(
|
|
|
4165
4172
|
archiveBelow: args.archiveBelow as number | undefined,
|
|
4166
4173
|
deleteBelow: args.deleteBelow as number | undefined,
|
|
4167
4174
|
limit: args.limit as number | undefined,
|
|
4175
|
+
staleDraftAgeDays: args.staleDraftAgeDays as number | undefined,
|
|
4168
4176
|
});
|
|
4169
4177
|
|
|
4170
4178
|
return {
|
|
@@ -4175,6 +4183,7 @@ async function handleToolCall(
|
|
|
4175
4183
|
legacyBreakdown: report.legacyBreakdown,
|
|
4176
4184
|
actionsTaken: report.actionsTaken,
|
|
4177
4185
|
lowest: report.lowest,
|
|
4186
|
+
staleDrafts: report.staleDrafts,
|
|
4178
4187
|
errors: report.errors,
|
|
4179
4188
|
healthReport: report.healthReport,
|
|
4180
4189
|
};
|
|
@@ -4210,7 +4219,14 @@ async function handleToolCall(
|
|
|
4210
4219
|
const report = await runMemoryCleanup(client, workspaceId, projectId, {
|
|
4211
4220
|
dryRun: args.dryRun as boolean | undefined,
|
|
4212
4221
|
steps: steps as
|
|
4213
|
-
| (
|
|
4222
|
+
| (
|
|
4223
|
+
| "prune"
|
|
4224
|
+
| "consolidate"
|
|
4225
|
+
| "orphans"
|
|
4226
|
+
| "duplicates"
|
|
4227
|
+
| "backfill"
|
|
4228
|
+
| "audit"
|
|
4229
|
+
)[]
|
|
4214
4230
|
| undefined,
|
|
4215
4231
|
maxAgeDays: args.maxAgeDays as number | undefined,
|
|
4216
4232
|
minClusterSize: args.minClusterSize as number | undefined,
|