@gethmy/mcp 2.3.4 → 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 +500 -19
- package/dist/index.js +500 -19
- package/package.json +2 -2
- package/src/__tests__/memory-audit.test.ts +386 -0
- package/src/memory-audit.ts +534 -0
- package/src/memory-cleanup.ts +89 -1
- package/src/server.ts +116 -7
package/dist/cli.js
CHANGED
|
@@ -28063,6 +28063,353 @@ async function runLifecycleMaintenance(client3, workspaceId, projectId) {
|
|
|
28063
28063
|
return result;
|
|
28064
28064
|
}
|
|
28065
28065
|
|
|
28066
|
+
// src/memory-audit.ts
|
|
28067
|
+
init_dist();
|
|
28068
|
+
var EMBEDDINGS_MIGRATION_AT = Date.parse("2026-02-18T00:00:00Z");
|
|
28069
|
+
var MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
28070
|
+
var BATCH_SIZE = 100;
|
|
28071
|
+
var CONCURRENCY_LIMIT = 5;
|
|
28072
|
+
var BOILERPLATE_PATTERNS = [
|
|
28073
|
+
/^todo:?$/i,
|
|
28074
|
+
/^placeholder/i,
|
|
28075
|
+
/^\.\.\.$/,
|
|
28076
|
+
/^untitled/i,
|
|
28077
|
+
/^(note|memo|draft)\s*\d*$/i
|
|
28078
|
+
];
|
|
28079
|
+
function isBoilerplate(title, content) {
|
|
28080
|
+
const t = title.trim();
|
|
28081
|
+
const c = content.trim();
|
|
28082
|
+
if (c.length === 0)
|
|
28083
|
+
return true;
|
|
28084
|
+
for (const pat of BOILERPLATE_PATTERNS) {
|
|
28085
|
+
if (pat.test(t))
|
|
28086
|
+
return true;
|
|
28087
|
+
}
|
|
28088
|
+
return false;
|
|
28089
|
+
}
|
|
28090
|
+
function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraftAgeDays) {
|
|
28091
|
+
const now = Date.now();
|
|
28092
|
+
const ageDays = (now - Date.parse(entity.created_at)) / MS_PER_DAY;
|
|
28093
|
+
const effectiveLastAccess = entity.last_accessed_at ?? entity.created_at;
|
|
28094
|
+
const lifecycle2 = evaluateLifecycle({
|
|
28095
|
+
memory_tier: entity.memory_tier,
|
|
28096
|
+
confidence: entity.confidence,
|
|
28097
|
+
access_count: entity.access_count,
|
|
28098
|
+
last_accessed_at: effectiveLastAccess,
|
|
28099
|
+
created_at: entity.created_at
|
|
28100
|
+
});
|
|
28101
|
+
const reasons = [];
|
|
28102
|
+
const legacyReasons = [];
|
|
28103
|
+
const confidence = Math.max(0, Math.min(1, entity.confidence)) * 25;
|
|
28104
|
+
const decay = Math.max(0, Math.min(1, lifecycle2.decay.score)) * 20;
|
|
28105
|
+
if (lifecycle2.decay.score < 0.2)
|
|
28106
|
+
reasons.push(`decay score ${lifecycle2.decay.score.toFixed(2)}`);
|
|
28107
|
+
const hasEmbedding = entity.embedding != null;
|
|
28108
|
+
const hasTags = (entity.tags?.length || 0) >= 1;
|
|
28109
|
+
const hasRelations = relationCount > 0;
|
|
28110
|
+
let structural = 0;
|
|
28111
|
+
if (hasEmbedding)
|
|
28112
|
+
structural += 6;
|
|
28113
|
+
if (hasTags)
|
|
28114
|
+
structural += 4;
|
|
28115
|
+
if (hasRelations)
|
|
28116
|
+
structural += 5;
|
|
28117
|
+
if (!hasEmbedding)
|
|
28118
|
+
reasons.push("no embedding");
|
|
28119
|
+
if (!hasTags)
|
|
28120
|
+
reasons.push("no tags");
|
|
28121
|
+
if (!hasRelations)
|
|
28122
|
+
reasons.push("no relations");
|
|
28123
|
+
let content = 0;
|
|
28124
|
+
const contentLen = entity.content?.length || 0;
|
|
28125
|
+
if (contentLen >= 80)
|
|
28126
|
+
content += 8;
|
|
28127
|
+
const titleOk = entity.title.trim().length >= 4 && !/^(untitled|draft|note)\b/i.test(entity.title.trim());
|
|
28128
|
+
if (titleOk)
|
|
28129
|
+
content += 4;
|
|
28130
|
+
if (!isBoilerplate(entity.title, entity.content))
|
|
28131
|
+
content += 3;
|
|
28132
|
+
if (contentLen < 80)
|
|
28133
|
+
reasons.push(`thin content (${contentLen} chars)`);
|
|
28134
|
+
if (isBoilerplate(entity.title, entity.content))
|
|
28135
|
+
reasons.push("boilerplate title/content");
|
|
28136
|
+
let tierAgeFit = 15;
|
|
28137
|
+
if (entity.memory_tier === "draft" && ageDays > 60 && !entity.promoted_from_id) {
|
|
28138
|
+
tierAgeFit = 0;
|
|
28139
|
+
reasons.push("stuck draft >60d never promoted");
|
|
28140
|
+
}
|
|
28141
|
+
if (entity.promoted_from_id) {
|
|
28142
|
+
tierAgeFit = Math.min(15, tierAgeFit + 5);
|
|
28143
|
+
}
|
|
28144
|
+
const access = Math.min(10, Math.log10((entity.access_count || 0) + 1) * 5);
|
|
28145
|
+
if (entity.access_count === 0 && ageDays > 14)
|
|
28146
|
+
reasons.push("never accessed");
|
|
28147
|
+
const raw = confidence + decay + structural + content + tierAgeFit + access;
|
|
28148
|
+
const score = Math.round(Math.max(0, Math.min(100, raw)));
|
|
28149
|
+
let legacy = false;
|
|
28150
|
+
if (entity.confidence === 1 && entity.access_count === 0 && ageDays > 30) {
|
|
28151
|
+
legacy = true;
|
|
28152
|
+
legacyReasons.push("default confidence never validated");
|
|
28153
|
+
}
|
|
28154
|
+
if (!hasEmbedding && Date.parse(entity.created_at) < EMBEDDINGS_MIGRATION_AT) {
|
|
28155
|
+
legacy = true;
|
|
28156
|
+
legacyReasons.push("pre-embeddings migration");
|
|
28157
|
+
}
|
|
28158
|
+
if (entity.memory_tier === "draft" && ageDays > 60 && !entity.promoted_from_id) {
|
|
28159
|
+
legacy = true;
|
|
28160
|
+
legacyReasons.push("stuck draft");
|
|
28161
|
+
}
|
|
28162
|
+
if (!hasTags && !hasRelations) {
|
|
28163
|
+
legacy = true;
|
|
28164
|
+
legacyReasons.push("no graph presence");
|
|
28165
|
+
}
|
|
28166
|
+
let bucket;
|
|
28167
|
+
if (score < deleteBelow)
|
|
28168
|
+
bucket = "delete";
|
|
28169
|
+
else if (score < archiveBelow)
|
|
28170
|
+
bucket = "archive";
|
|
28171
|
+
else if (score < 70)
|
|
28172
|
+
bucket = "review";
|
|
28173
|
+
else
|
|
28174
|
+
bucket = "keep";
|
|
28175
|
+
const staleDraft = entity.memory_tier === "draft" && (entity.access_count || 0) === 0 && ageDays > staleDraftAgeDays;
|
|
28176
|
+
return {
|
|
28177
|
+
id: entity.id,
|
|
28178
|
+
title: entity.title,
|
|
28179
|
+
type: entity.type,
|
|
28180
|
+
tier: entity.memory_tier,
|
|
28181
|
+
ageDays: Math.round(ageDays),
|
|
28182
|
+
score,
|
|
28183
|
+
bucket,
|
|
28184
|
+
reasons,
|
|
28185
|
+
legacy,
|
|
28186
|
+
legacyReasons,
|
|
28187
|
+
staleDraft,
|
|
28188
|
+
subScores: {
|
|
28189
|
+
confidence: Math.round(confidence),
|
|
28190
|
+
decay: Math.round(decay),
|
|
28191
|
+
structural,
|
|
28192
|
+
content,
|
|
28193
|
+
tierAgeFit,
|
|
28194
|
+
access: Math.round(access)
|
|
28195
|
+
}
|
|
28196
|
+
};
|
|
28197
|
+
}
|
|
28198
|
+
async function runMemoryAudit(client3, workspaceId, projectId, options) {
|
|
28199
|
+
const dryRun = options?.dryRun !== false;
|
|
28200
|
+
const archiveBelow = options?.archiveBelow ?? 40;
|
|
28201
|
+
const deleteBelow = options?.deleteBelow ?? 20;
|
|
28202
|
+
const limit = options?.limit ?? 500;
|
|
28203
|
+
const staleDraftAgeDays = options?.staleDraftAgeDays ?? 7;
|
|
28204
|
+
const report = {
|
|
28205
|
+
success: true,
|
|
28206
|
+
dryRun,
|
|
28207
|
+
timestamp: new Date().toISOString(),
|
|
28208
|
+
workspace: { id: workspaceId, projectId },
|
|
28209
|
+
summary: {
|
|
28210
|
+
totalEntities: 0,
|
|
28211
|
+
scanned: 0,
|
|
28212
|
+
keep: 0,
|
|
28213
|
+
review: 0,
|
|
28214
|
+
archive: 0,
|
|
28215
|
+
delete: 0,
|
|
28216
|
+
legacyCount: 0,
|
|
28217
|
+
staleDraftCount: 0
|
|
28218
|
+
},
|
|
28219
|
+
actionsTaken: { flaggedReview: 0, archived: 0, deleted: 0 },
|
|
28220
|
+
distribution: { "0-20": 0, "20-40": 0, "40-70": 0, "70-100": 0 },
|
|
28221
|
+
legacyBreakdown: {
|
|
28222
|
+
defaultConfidence: 0,
|
|
28223
|
+
missingEmbedding: 0,
|
|
28224
|
+
stuckDraft: 0,
|
|
28225
|
+
noGraphPresence: 0
|
|
28226
|
+
},
|
|
28227
|
+
lowest: [],
|
|
28228
|
+
staleDrafts: [],
|
|
28229
|
+
errors: [],
|
|
28230
|
+
healthReport: ""
|
|
28231
|
+
};
|
|
28232
|
+
const entities = [];
|
|
28233
|
+
let offset = 0;
|
|
28234
|
+
try {
|
|
28235
|
+
while (entities.length < limit) {
|
|
28236
|
+
const pageSize = Math.min(BATCH_SIZE, limit - entities.length);
|
|
28237
|
+
const result = await client3.listMemoryEntities({
|
|
28238
|
+
workspace_id: workspaceId,
|
|
28239
|
+
project_id: projectId,
|
|
28240
|
+
limit: pageSize,
|
|
28241
|
+
offset
|
|
28242
|
+
});
|
|
28243
|
+
const page = result.entities || [];
|
|
28244
|
+
if (page.length === 0)
|
|
28245
|
+
break;
|
|
28246
|
+
entities.push(...page);
|
|
28247
|
+
if (page.length < pageSize)
|
|
28248
|
+
break;
|
|
28249
|
+
offset += pageSize;
|
|
28250
|
+
}
|
|
28251
|
+
} catch (err) {
|
|
28252
|
+
report.errors.push({
|
|
28253
|
+
step: "fetch",
|
|
28254
|
+
message: `Failed to fetch entities: ${err.message}`
|
|
28255
|
+
});
|
|
28256
|
+
report.success = false;
|
|
28257
|
+
report.healthReport = renderReport(report);
|
|
28258
|
+
return report;
|
|
28259
|
+
}
|
|
28260
|
+
report.summary.totalEntities = entities.length;
|
|
28261
|
+
const relationCounts = new Map;
|
|
28262
|
+
for (let i = 0;i < entities.length; i += CONCURRENCY_LIMIT) {
|
|
28263
|
+
const batch = entities.slice(i, i + CONCURRENCY_LIMIT);
|
|
28264
|
+
const results = await Promise.allSettled(batch.map(async (e) => {
|
|
28265
|
+
const related = await client3.getRelatedEntities(e.id);
|
|
28266
|
+
const count = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
|
|
28267
|
+
return { id: e.id, count };
|
|
28268
|
+
}));
|
|
28269
|
+
for (const r of results) {
|
|
28270
|
+
if (r.status === "fulfilled") {
|
|
28271
|
+
relationCounts.set(r.value.id, r.value.count);
|
|
28272
|
+
}
|
|
28273
|
+
}
|
|
28274
|
+
}
|
|
28275
|
+
const audits = [];
|
|
28276
|
+
for (const entity of entities) {
|
|
28277
|
+
const relCount = relationCounts.get(entity.id) ?? 0;
|
|
28278
|
+
const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow, staleDraftAgeDays);
|
|
28279
|
+
audits.push(audit);
|
|
28280
|
+
report.summary.scanned++;
|
|
28281
|
+
report.summary[audit.bucket]++;
|
|
28282
|
+
if (audit.legacy)
|
|
28283
|
+
report.summary.legacyCount++;
|
|
28284
|
+
if (audit.staleDraft)
|
|
28285
|
+
report.summary.staleDraftCount++;
|
|
28286
|
+
if (audit.score < 20)
|
|
28287
|
+
report.distribution["0-20"]++;
|
|
28288
|
+
else if (audit.score < 40)
|
|
28289
|
+
report.distribution["20-40"]++;
|
|
28290
|
+
else if (audit.score < 70)
|
|
28291
|
+
report.distribution["40-70"]++;
|
|
28292
|
+
else
|
|
28293
|
+
report.distribution["70-100"]++;
|
|
28294
|
+
for (const reason of audit.legacyReasons) {
|
|
28295
|
+
if (reason.startsWith("default confidence"))
|
|
28296
|
+
report.legacyBreakdown.defaultConfidence++;
|
|
28297
|
+
else if (reason.startsWith("pre-embeddings"))
|
|
28298
|
+
report.legacyBreakdown.missingEmbedding++;
|
|
28299
|
+
else if (reason.startsWith("stuck draft"))
|
|
28300
|
+
report.legacyBreakdown.stuckDraft++;
|
|
28301
|
+
else if (reason.startsWith("no graph"))
|
|
28302
|
+
report.legacyBreakdown.noGraphPresence++;
|
|
28303
|
+
}
|
|
28304
|
+
}
|
|
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);
|
|
28307
|
+
if (!dryRun) {
|
|
28308
|
+
for (const audit of audits) {
|
|
28309
|
+
try {
|
|
28310
|
+
if (audit.bucket === "delete") {
|
|
28311
|
+
await client3.deleteMemoryEntity(audit.id);
|
|
28312
|
+
report.actionsTaken.deleted++;
|
|
28313
|
+
} else if (audit.bucket === "archive") {
|
|
28314
|
+
await client3.updateMemoryEntity(audit.id, {
|
|
28315
|
+
confidence: 0.25,
|
|
28316
|
+
metadata: {
|
|
28317
|
+
audit_archived_at: new Date().toISOString(),
|
|
28318
|
+
audit_score: audit.score,
|
|
28319
|
+
audit_reasons: audit.reasons
|
|
28320
|
+
}
|
|
28321
|
+
});
|
|
28322
|
+
report.actionsTaken.archived++;
|
|
28323
|
+
} else if (audit.bucket === "review") {
|
|
28324
|
+
await client3.updateMemoryEntity(audit.id, {
|
|
28325
|
+
metadata: {
|
|
28326
|
+
needs_review: true,
|
|
28327
|
+
audit_score: audit.score,
|
|
28328
|
+
audit_reasons: audit.reasons,
|
|
28329
|
+
audit_at: new Date().toISOString()
|
|
28330
|
+
}
|
|
28331
|
+
});
|
|
28332
|
+
report.actionsTaken.flaggedReview++;
|
|
28333
|
+
}
|
|
28334
|
+
} catch (err) {
|
|
28335
|
+
report.errors.push({
|
|
28336
|
+
entityId: audit.id,
|
|
28337
|
+
step: audit.bucket,
|
|
28338
|
+
message: err.message
|
|
28339
|
+
});
|
|
28340
|
+
}
|
|
28341
|
+
}
|
|
28342
|
+
}
|
|
28343
|
+
report.healthReport = renderReport(report);
|
|
28344
|
+
return report;
|
|
28345
|
+
}
|
|
28346
|
+
function renderReport(report) {
|
|
28347
|
+
const mode = report.dryRun ? "Dry Run (preview)" : "Executed";
|
|
28348
|
+
const s = report.summary;
|
|
28349
|
+
const lines = [
|
|
28350
|
+
`# Memory Quality Audit
|
|
28351
|
+
`,
|
|
28352
|
+
`**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount} | **Stale drafts:** ${s.staleDraftCount}`,
|
|
28353
|
+
"",
|
|
28354
|
+
"## Distribution",
|
|
28355
|
+
`- 70-100 (keep): ${report.distribution["70-100"]}`,
|
|
28356
|
+
`- 40-69 (review): ${report.distribution["40-70"]}`,
|
|
28357
|
+
`- 20-39 (archive): ${report.distribution["20-40"]}`,
|
|
28358
|
+
`- 0-19 (delete): ${report.distribution["0-20"]}`,
|
|
28359
|
+
"",
|
|
28360
|
+
"## Buckets",
|
|
28361
|
+
`- **Keep:** ${s.keep}`,
|
|
28362
|
+
`- **Review:** ${s.review}${!report.dryRun ? ` (flagged ${report.actionsTaken.flaggedReview})` : ""}`,
|
|
28363
|
+
`- **Archive:** ${s.archive}${!report.dryRun ? ` (archived ${report.actionsTaken.archived})` : ""}`,
|
|
28364
|
+
`- **Delete:** ${s.delete}${!report.dryRun ? ` (deleted ${report.actionsTaken.deleted})` : ""}`,
|
|
28365
|
+
""
|
|
28366
|
+
];
|
|
28367
|
+
const l = report.legacyBreakdown;
|
|
28368
|
+
if (s.legacyCount > 0) {
|
|
28369
|
+
lines.push("## Legacy Breakdown");
|
|
28370
|
+
lines.push(`- Default confidence, never validated: ${l.defaultConfidence}`);
|
|
28371
|
+
lines.push(`- Pre-embeddings migration: ${l.missingEmbedding}`);
|
|
28372
|
+
lines.push(`- Stuck drafts (>60d, no promotion): ${l.stuckDraft}`);
|
|
28373
|
+
lines.push(`- No tags + no relations: ${l.noGraphPresence}`);
|
|
28374
|
+
lines.push("");
|
|
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
|
+
}
|
|
28387
|
+
if (report.lowest.length > 0) {
|
|
28388
|
+
lines.push("## Lowest-Scoring (top 10)");
|
|
28389
|
+
lines.push("| Score | Bucket | Tier | Age | Title | Reasons |");
|
|
28390
|
+
lines.push("|-------|--------|------|-----|-------|---------|");
|
|
28391
|
+
for (const a of report.lowest) {
|
|
28392
|
+
const reasonStr = a.reasons.slice(0, 3).join(", ") || "—";
|
|
28393
|
+
const titleTrunc = a.title.length > 40 ? `${a.title.slice(0, 37)}...` : a.title;
|
|
28394
|
+
lines.push(`| ${a.score} | ${a.bucket} | ${a.tier} | ${a.ageDays}d | ${titleTrunc} | ${reasonStr} |`);
|
|
28395
|
+
}
|
|
28396
|
+
lines.push("");
|
|
28397
|
+
}
|
|
28398
|
+
if (report.errors.length > 0) {
|
|
28399
|
+
lines.push("## Errors");
|
|
28400
|
+
for (const e of report.errors.slice(0, 10)) {
|
|
28401
|
+
lines.push(`- **${e.step}${e.entityId ? ` ${e.entityId}` : ""}:** ${e.message}`);
|
|
28402
|
+
}
|
|
28403
|
+
lines.push("");
|
|
28404
|
+
}
|
|
28405
|
+
if (report.dryRun) {
|
|
28406
|
+
lines.push("---");
|
|
28407
|
+
lines.push("*Run with `dryRun: false` to flag review entries, archive low-quality memories, and delete worst offenders.*");
|
|
28408
|
+
}
|
|
28409
|
+
return lines.join(`
|
|
28410
|
+
`);
|
|
28411
|
+
}
|
|
28412
|
+
|
|
28066
28413
|
// src/memory-cleanup.ts
|
|
28067
28414
|
init_dist();
|
|
28068
28415
|
var ALL_STEPS = [
|
|
@@ -28070,12 +28417,13 @@ var ALL_STEPS = [
|
|
|
28070
28417
|
"consolidate",
|
|
28071
28418
|
"orphans",
|
|
28072
28419
|
"duplicates",
|
|
28073
|
-
"backfill"
|
|
28420
|
+
"backfill",
|
|
28421
|
+
"audit"
|
|
28074
28422
|
];
|
|
28075
|
-
var
|
|
28423
|
+
var MS_PER_DAY2 = 1000 * 60 * 60 * 24;
|
|
28076
28424
|
var MAX_ENTITIES_FETCH = 200;
|
|
28077
28425
|
var DUPLICATE_SIMILARITY_THRESHOLD = 0.85;
|
|
28078
|
-
var
|
|
28426
|
+
var CONCURRENCY_LIMIT2 = 5;
|
|
28079
28427
|
async function runMemoryCleanup(client3, workspaceId, projectId, options) {
|
|
28080
28428
|
const dryRun = options?.dryRun !== false;
|
|
28081
28429
|
const steps = options?.steps ?? ALL_STEPS;
|
|
@@ -28231,6 +28579,44 @@ async function runMemoryCleanup(client3, workspaceId, projectId, options) {
|
|
|
28231
28579
|
});
|
|
28232
28580
|
}
|
|
28233
28581
|
}
|
|
28582
|
+
if (steps.includes("audit")) {
|
|
28583
|
+
try {
|
|
28584
|
+
const auditReport = await runMemoryAudit(client3, workspaceId, projectId, {
|
|
28585
|
+
dryRun,
|
|
28586
|
+
archiveBelow: options?.auditArchiveBelow,
|
|
28587
|
+
deleteBelow: options?.auditDeleteBelow
|
|
28588
|
+
});
|
|
28589
|
+
const low = auditReport.lowest.length > 0 ? auditReport.lowest[0].score : null;
|
|
28590
|
+
report.steps.audit = {
|
|
28591
|
+
scanned: auditReport.summary.scanned,
|
|
28592
|
+
legacyCount: auditReport.summary.legacyCount,
|
|
28593
|
+
buckets: {
|
|
28594
|
+
keep: auditReport.summary.keep,
|
|
28595
|
+
review: auditReport.summary.review,
|
|
28596
|
+
archive: auditReport.summary.archive,
|
|
28597
|
+
delete: auditReport.summary.delete
|
|
28598
|
+
},
|
|
28599
|
+
actions: auditReport.actionsTaken,
|
|
28600
|
+
lowestScore: low,
|
|
28601
|
+
report: auditReport
|
|
28602
|
+
};
|
|
28603
|
+
report.summary.issuesFound += auditReport.summary.review + auditReport.summary.archive + auditReport.summary.delete;
|
|
28604
|
+
if (!dryRun) {
|
|
28605
|
+
report.summary.actionsTaken += auditReport.actionsTaken.flaggedReview + auditReport.actionsTaken.archived + auditReport.actionsTaken.deleted;
|
|
28606
|
+
}
|
|
28607
|
+
for (const err of auditReport.errors) {
|
|
28608
|
+
report.errors.push({
|
|
28609
|
+
step: `audit:${err.step}`,
|
|
28610
|
+
message: err.entityId ? `${err.entityId}: ${err.message}` : err.message
|
|
28611
|
+
});
|
|
28612
|
+
}
|
|
28613
|
+
} catch (err) {
|
|
28614
|
+
report.errors.push({
|
|
28615
|
+
step: "audit",
|
|
28616
|
+
message: err.message
|
|
28617
|
+
});
|
|
28618
|
+
}
|
|
28619
|
+
}
|
|
28234
28620
|
report.healthReport = generateHealthReport(report);
|
|
28235
28621
|
return report;
|
|
28236
28622
|
}
|
|
@@ -28239,7 +28625,7 @@ function runPruneStep(entities, maxAgeDays) {
|
|
|
28239
28625
|
const drafts = entities.filter((e) => e.memory_tier === "draft");
|
|
28240
28626
|
const stale = [];
|
|
28241
28627
|
for (const entity of drafts) {
|
|
28242
|
-
const ageDays = (now - new Date(entity.created_at).getTime()) /
|
|
28628
|
+
const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY2;
|
|
28243
28629
|
if (ageDays < maxAgeDays)
|
|
28244
28630
|
continue;
|
|
28245
28631
|
const lifecycle2 = evaluateLifecycle(entity);
|
|
@@ -28260,17 +28646,17 @@ async function runOrphanStep(client3, entities, orphanAgeDays) {
|
|
|
28260
28646
|
return false;
|
|
28261
28647
|
if (e.access_count >= 2)
|
|
28262
28648
|
return false;
|
|
28263
|
-
const ageDays = (now - new Date(e.created_at).getTime()) /
|
|
28649
|
+
const ageDays = (now - new Date(e.created_at).getTime()) / MS_PER_DAY2;
|
|
28264
28650
|
return ageDays >= orphanAgeDays;
|
|
28265
28651
|
});
|
|
28266
|
-
for (let i = 0;i < candidates.length; i +=
|
|
28267
|
-
const batch = candidates.slice(i, i +
|
|
28652
|
+
for (let i = 0;i < candidates.length; i += CONCURRENCY_LIMIT2) {
|
|
28653
|
+
const batch = candidates.slice(i, i + CONCURRENCY_LIMIT2);
|
|
28268
28654
|
const results = await Promise.allSettled(batch.map(async (entity) => {
|
|
28269
28655
|
const related = await client3.getRelatedEntities(entity.id);
|
|
28270
28656
|
const totalRelations = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
|
|
28271
28657
|
if (totalRelations > 0)
|
|
28272
28658
|
return null;
|
|
28273
|
-
const ageDays = (now - new Date(entity.created_at).getTime()) /
|
|
28659
|
+
const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY2;
|
|
28274
28660
|
return {
|
|
28275
28661
|
id: entity.id,
|
|
28276
28662
|
title: entity.title,
|
|
@@ -28299,8 +28685,8 @@ async function runDuplicateStep(client3, entities, workspaceId, projectId) {
|
|
|
28299
28685
|
const flaggedForRemoval = new Set;
|
|
28300
28686
|
const entityMap = new Map(entities.map((e) => [e.id, e]));
|
|
28301
28687
|
const similarityMap = new Map;
|
|
28302
|
-
for (let i = 0;i < entities.length; i +=
|
|
28303
|
-
const batch = entities.slice(i, i +
|
|
28688
|
+
for (let i = 0;i < entities.length; i += CONCURRENCY_LIMIT2) {
|
|
28689
|
+
const batch = entities.slice(i, i + CONCURRENCY_LIMIT2);
|
|
28304
28690
|
const results = await Promise.allSettled(batch.map(async (entity) => {
|
|
28305
28691
|
const similar = await findSimilarEntities(client3, entity.title, entity.content, workspaceId, { projectId, limit: 5, minRrfScore: 0.05, excludeIds: [entity.id] });
|
|
28306
28692
|
return { entityId: entity.id, similar };
|
|
@@ -28452,6 +28838,20 @@ function generateHealthReport(report) {
|
|
|
28452
28838
|
`);
|
|
28453
28839
|
}
|
|
28454
28840
|
}
|
|
28841
|
+
if (report.steps.audit) {
|
|
28842
|
+
const a = report.steps.audit;
|
|
28843
|
+
lines.push("## Quality Audit");
|
|
28844
|
+
lines.push(`Scanned ${a.scanned} entities. Legacy signals on ${a.legacyCount}.`);
|
|
28845
|
+
lines.push(`Buckets — keep: ${a.buckets.keep}, review: ${a.buckets.review}, archive: ${a.buckets.archive}, delete: ${a.buckets.delete}.`);
|
|
28846
|
+
if (!report.dryRun) {
|
|
28847
|
+
lines.push(`Actions — flagged: ${a.actions.flaggedReview}, archived: ${a.actions.archived}, deleted: ${a.actions.deleted}.`);
|
|
28848
|
+
}
|
|
28849
|
+
if (a.report.lowest.length > 0) {
|
|
28850
|
+
const worst = a.report.lowest[0];
|
|
28851
|
+
lines.push(`Lowest score: **${worst.score}** — "${worst.title}" (${worst.reasons.slice(0, 2).join(", ") || "—"}).`);
|
|
28852
|
+
}
|
|
28853
|
+
lines.push("");
|
|
28854
|
+
}
|
|
28455
28855
|
if (report.errors.length > 0) {
|
|
28456
28856
|
lines.push("## Errors");
|
|
28457
28857
|
for (const e of report.errors) {
|
|
@@ -28496,7 +28896,7 @@ async function purgeMemories(client3, workspaceId, projectId, options) {
|
|
|
28496
28896
|
continue;
|
|
28497
28897
|
if (filters.olderThanDays !== undefined) {
|
|
28498
28898
|
const ref = entity.last_accessed_at || entity.created_at;
|
|
28499
|
-
const ageDays = (now - new Date(ref).getTime()) /
|
|
28899
|
+
const ageDays = (now - new Date(ref).getTime()) / MS_PER_DAY2;
|
|
28500
28900
|
if (ageDays < filters.olderThanDays)
|
|
28501
28901
|
continue;
|
|
28502
28902
|
}
|
|
@@ -28512,13 +28912,13 @@ async function purgeMemories(client3, workspaceId, projectId, options) {
|
|
|
28512
28912
|
type: e.type,
|
|
28513
28913
|
tier: e.memory_tier,
|
|
28514
28914
|
confidence: e.confidence,
|
|
28515
|
-
ageDays: Math.round((now - new Date(e.last_accessed_at || e.created_at).getTime()) /
|
|
28915
|
+
ageDays: Math.round((now - new Date(e.last_accessed_at || e.created_at).getTime()) / MS_PER_DAY2)
|
|
28516
28916
|
}));
|
|
28517
28917
|
const errors3 = [];
|
|
28518
28918
|
let purged = 0;
|
|
28519
28919
|
if (!dryRun) {
|
|
28520
|
-
for (let i = 0;i < allMatches.length; i +=
|
|
28521
|
-
const batch = allMatches.slice(i, i +
|
|
28920
|
+
for (let i = 0;i < allMatches.length; i += CONCURRENCY_LIMIT2) {
|
|
28921
|
+
const batch = allMatches.slice(i, i + CONCURRENCY_LIMIT2);
|
|
28522
28922
|
const results = await Promise.allSettled(batch.map((e) => client3.deleteMemoryEntity(e.id)));
|
|
28523
28923
|
for (let j = 0;j < results.length; j++) {
|
|
28524
28924
|
if (results[j].status === "fulfilled") {
|
|
@@ -29995,7 +30395,7 @@ var TOOLS = {
|
|
|
29995
30395
|
}
|
|
29996
30396
|
},
|
|
29997
30397
|
harmony_cleanup_memories: {
|
|
29998
|
-
description: "Run a unified memory cleanup: prune stale drafts, consolidate similar memories, detect orphans and duplicates, and
|
|
30398
|
+
description: "Run a unified memory cleanup: prune stale drafts, consolidate similar memories, detect orphans and duplicates, backfill embeddings, and optionally run a quality audit. Returns a health report. Dry-run by default — run with dryRun=false to execute.",
|
|
29999
30399
|
inputSchema: {
|
|
30000
30400
|
type: "object",
|
|
30001
30401
|
properties: {
|
|
@@ -30015,9 +30415,16 @@ var TOOLS = {
|
|
|
30015
30415
|
type: "array",
|
|
30016
30416
|
items: {
|
|
30017
30417
|
type: "string",
|
|
30018
|
-
enum: [
|
|
30418
|
+
enum: [
|
|
30419
|
+
"prune",
|
|
30420
|
+
"consolidate",
|
|
30421
|
+
"orphans",
|
|
30422
|
+
"duplicates",
|
|
30423
|
+
"backfill",
|
|
30424
|
+
"audit"
|
|
30425
|
+
]
|
|
30019
30426
|
},
|
|
30020
|
-
description: "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill."
|
|
30427
|
+
description: "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill, audit."
|
|
30021
30428
|
},
|
|
30022
30429
|
maxAgeDays: {
|
|
30023
30430
|
type: "number",
|
|
@@ -30030,6 +30437,51 @@ var TOOLS = {
|
|
|
30030
30437
|
orphanAgeDays: {
|
|
30031
30438
|
type: "number",
|
|
30032
30439
|
description: "Min age in days for orphan detection (default: 14)"
|
|
30440
|
+
},
|
|
30441
|
+
auditArchiveBelow: {
|
|
30442
|
+
type: "number",
|
|
30443
|
+
description: "Audit: archive entities scoring below this (default: 40)"
|
|
30444
|
+
},
|
|
30445
|
+
auditDeleteBelow: {
|
|
30446
|
+
type: "number",
|
|
30447
|
+
description: "Audit: delete entities scoring below this (default: 20)"
|
|
30448
|
+
}
|
|
30449
|
+
},
|
|
30450
|
+
required: []
|
|
30451
|
+
}
|
|
30452
|
+
},
|
|
30453
|
+
harmony_audit_memories: {
|
|
30454
|
+
description: "Rate every memory against state-of-the-art quality standards (confidence, decay, structural completeness, content, tier-age fit, access). Flags legacy entities from before recent optimizations (default confidence, missing embeddings, stuck drafts). Buckets: keep (≥70), review (40-69), archive (20-39), delete (<20). Dry-run by default.",
|
|
30455
|
+
inputSchema: {
|
|
30456
|
+
type: "object",
|
|
30457
|
+
properties: {
|
|
30458
|
+
workspaceId: {
|
|
30459
|
+
type: "string",
|
|
30460
|
+
description: "Workspace ID (optional if context set)"
|
|
30461
|
+
},
|
|
30462
|
+
projectId: {
|
|
30463
|
+
type: "string",
|
|
30464
|
+
description: "Project ID (optional)"
|
|
30465
|
+
},
|
|
30466
|
+
dryRun: {
|
|
30467
|
+
type: "boolean",
|
|
30468
|
+
description: "Preview audit without flagging/archiving/deleting (default: true)"
|
|
30469
|
+
},
|
|
30470
|
+
archiveBelow: {
|
|
30471
|
+
type: "number",
|
|
30472
|
+
description: "Score threshold below which entities are archived (confidence set to 0.25). Default: 40"
|
|
30473
|
+
},
|
|
30474
|
+
deleteBelow: {
|
|
30475
|
+
type: "number",
|
|
30476
|
+
description: "Score threshold below which entities are hard-deleted. Default: 20. Set to 0 to never delete."
|
|
30477
|
+
},
|
|
30478
|
+
limit: {
|
|
30479
|
+
type: "number",
|
|
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."
|
|
30033
30485
|
}
|
|
30034
30486
|
},
|
|
30035
30487
|
required: []
|
|
@@ -31599,6 +32051,32 @@ async function handleToolCall(name, args, deps) {
|
|
|
31599
32051
|
message: `Processed ${entitiesProcessed} entities, created ${relationsCreated} new relations.`
|
|
31600
32052
|
};
|
|
31601
32053
|
}
|
|
32054
|
+
case "harmony_audit_memories": {
|
|
32055
|
+
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
|
32056
|
+
if (!workspaceId) {
|
|
32057
|
+
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
32058
|
+
}
|
|
32059
|
+
const projectId = args.projectId || deps.getActiveProjectId() || undefined;
|
|
32060
|
+
const report = await runMemoryAudit(client3, workspaceId, projectId, {
|
|
32061
|
+
dryRun: args.dryRun,
|
|
32062
|
+
archiveBelow: args.archiveBelow,
|
|
32063
|
+
deleteBelow: args.deleteBelow,
|
|
32064
|
+
limit: args.limit,
|
|
32065
|
+
staleDraftAgeDays: args.staleDraftAgeDays
|
|
32066
|
+
});
|
|
32067
|
+
return {
|
|
32068
|
+
success: report.success,
|
|
32069
|
+
dryRun: report.dryRun,
|
|
32070
|
+
summary: report.summary,
|
|
32071
|
+
distribution: report.distribution,
|
|
32072
|
+
legacyBreakdown: report.legacyBreakdown,
|
|
32073
|
+
actionsTaken: report.actionsTaken,
|
|
32074
|
+
lowest: report.lowest,
|
|
32075
|
+
staleDrafts: report.staleDrafts,
|
|
32076
|
+
errors: report.errors,
|
|
32077
|
+
healthReport: report.healthReport
|
|
32078
|
+
};
|
|
32079
|
+
}
|
|
31602
32080
|
case "harmony_cleanup_memories": {
|
|
31603
32081
|
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
|
31604
32082
|
if (!workspaceId) {
|
|
@@ -31610,7 +32088,8 @@ async function handleToolCall(name, args, deps) {
|
|
|
31610
32088
|
"consolidate",
|
|
31611
32089
|
"orphans",
|
|
31612
32090
|
"duplicates",
|
|
31613
|
-
"backfill"
|
|
32091
|
+
"backfill",
|
|
32092
|
+
"audit"
|
|
31614
32093
|
];
|
|
31615
32094
|
const rawSteps = args.steps;
|
|
31616
32095
|
const steps = rawSteps?.filter((s) => validSteps.includes(s));
|
|
@@ -31623,7 +32102,9 @@ async function handleToolCall(name, args, deps) {
|
|
|
31623
32102
|
steps,
|
|
31624
32103
|
maxAgeDays: args.maxAgeDays,
|
|
31625
32104
|
minClusterSize: args.minClusterSize,
|
|
31626
|
-
orphanAgeDays: args.orphanAgeDays
|
|
32105
|
+
orphanAgeDays: args.orphanAgeDays,
|
|
32106
|
+
auditArchiveBelow: args.auditArchiveBelow,
|
|
32107
|
+
auditDeleteBelow: args.auditDeleteBelow
|
|
31627
32108
|
});
|
|
31628
32109
|
return {
|
|
31629
32110
|
success: report.success,
|