@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/index.js
CHANGED
|
@@ -25823,6 +25823,353 @@ async function runLifecycleMaintenance(client3, workspaceId, projectId) {
|
|
|
25823
25823
|
return result;
|
|
25824
25824
|
}
|
|
25825
25825
|
|
|
25826
|
+
// src/memory-audit.ts
|
|
25827
|
+
init_dist();
|
|
25828
|
+
var EMBEDDINGS_MIGRATION_AT = Date.parse("2026-02-18T00:00:00Z");
|
|
25829
|
+
var MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
25830
|
+
var BATCH_SIZE = 100;
|
|
25831
|
+
var CONCURRENCY_LIMIT = 5;
|
|
25832
|
+
var BOILERPLATE_PATTERNS = [
|
|
25833
|
+
/^todo:?$/i,
|
|
25834
|
+
/^placeholder/i,
|
|
25835
|
+
/^\.\.\.$/,
|
|
25836
|
+
/^untitled/i,
|
|
25837
|
+
/^(note|memo|draft)\s*\d*$/i
|
|
25838
|
+
];
|
|
25839
|
+
function isBoilerplate(title, content) {
|
|
25840
|
+
const t = title.trim();
|
|
25841
|
+
const c = content.trim();
|
|
25842
|
+
if (c.length === 0)
|
|
25843
|
+
return true;
|
|
25844
|
+
for (const pat of BOILERPLATE_PATTERNS) {
|
|
25845
|
+
if (pat.test(t))
|
|
25846
|
+
return true;
|
|
25847
|
+
}
|
|
25848
|
+
return false;
|
|
25849
|
+
}
|
|
25850
|
+
function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraftAgeDays) {
|
|
25851
|
+
const now = Date.now();
|
|
25852
|
+
const ageDays = (now - Date.parse(entity.created_at)) / MS_PER_DAY;
|
|
25853
|
+
const effectiveLastAccess = entity.last_accessed_at ?? entity.created_at;
|
|
25854
|
+
const lifecycle2 = evaluateLifecycle({
|
|
25855
|
+
memory_tier: entity.memory_tier,
|
|
25856
|
+
confidence: entity.confidence,
|
|
25857
|
+
access_count: entity.access_count,
|
|
25858
|
+
last_accessed_at: effectiveLastAccess,
|
|
25859
|
+
created_at: entity.created_at
|
|
25860
|
+
});
|
|
25861
|
+
const reasons = [];
|
|
25862
|
+
const legacyReasons = [];
|
|
25863
|
+
const confidence = Math.max(0, Math.min(1, entity.confidence)) * 25;
|
|
25864
|
+
const decay = Math.max(0, Math.min(1, lifecycle2.decay.score)) * 20;
|
|
25865
|
+
if (lifecycle2.decay.score < 0.2)
|
|
25866
|
+
reasons.push(`decay score ${lifecycle2.decay.score.toFixed(2)}`);
|
|
25867
|
+
const hasEmbedding = entity.embedding != null;
|
|
25868
|
+
const hasTags = (entity.tags?.length || 0) >= 1;
|
|
25869
|
+
const hasRelations = relationCount > 0;
|
|
25870
|
+
let structural = 0;
|
|
25871
|
+
if (hasEmbedding)
|
|
25872
|
+
structural += 6;
|
|
25873
|
+
if (hasTags)
|
|
25874
|
+
structural += 4;
|
|
25875
|
+
if (hasRelations)
|
|
25876
|
+
structural += 5;
|
|
25877
|
+
if (!hasEmbedding)
|
|
25878
|
+
reasons.push("no embedding");
|
|
25879
|
+
if (!hasTags)
|
|
25880
|
+
reasons.push("no tags");
|
|
25881
|
+
if (!hasRelations)
|
|
25882
|
+
reasons.push("no relations");
|
|
25883
|
+
let content = 0;
|
|
25884
|
+
const contentLen = entity.content?.length || 0;
|
|
25885
|
+
if (contentLen >= 80)
|
|
25886
|
+
content += 8;
|
|
25887
|
+
const titleOk = entity.title.trim().length >= 4 && !/^(untitled|draft|note)\b/i.test(entity.title.trim());
|
|
25888
|
+
if (titleOk)
|
|
25889
|
+
content += 4;
|
|
25890
|
+
if (!isBoilerplate(entity.title, entity.content))
|
|
25891
|
+
content += 3;
|
|
25892
|
+
if (contentLen < 80)
|
|
25893
|
+
reasons.push(`thin content (${contentLen} chars)`);
|
|
25894
|
+
if (isBoilerplate(entity.title, entity.content))
|
|
25895
|
+
reasons.push("boilerplate title/content");
|
|
25896
|
+
let tierAgeFit = 15;
|
|
25897
|
+
if (entity.memory_tier === "draft" && ageDays > 60 && !entity.promoted_from_id) {
|
|
25898
|
+
tierAgeFit = 0;
|
|
25899
|
+
reasons.push("stuck draft >60d never promoted");
|
|
25900
|
+
}
|
|
25901
|
+
if (entity.promoted_from_id) {
|
|
25902
|
+
tierAgeFit = Math.min(15, tierAgeFit + 5);
|
|
25903
|
+
}
|
|
25904
|
+
const access = Math.min(10, Math.log10((entity.access_count || 0) + 1) * 5);
|
|
25905
|
+
if (entity.access_count === 0 && ageDays > 14)
|
|
25906
|
+
reasons.push("never accessed");
|
|
25907
|
+
const raw = confidence + decay + structural + content + tierAgeFit + access;
|
|
25908
|
+
const score = Math.round(Math.max(0, Math.min(100, raw)));
|
|
25909
|
+
let legacy = false;
|
|
25910
|
+
if (entity.confidence === 1 && entity.access_count === 0 && ageDays > 30) {
|
|
25911
|
+
legacy = true;
|
|
25912
|
+
legacyReasons.push("default confidence never validated");
|
|
25913
|
+
}
|
|
25914
|
+
if (!hasEmbedding && Date.parse(entity.created_at) < EMBEDDINGS_MIGRATION_AT) {
|
|
25915
|
+
legacy = true;
|
|
25916
|
+
legacyReasons.push("pre-embeddings migration");
|
|
25917
|
+
}
|
|
25918
|
+
if (entity.memory_tier === "draft" && ageDays > 60 && !entity.promoted_from_id) {
|
|
25919
|
+
legacy = true;
|
|
25920
|
+
legacyReasons.push("stuck draft");
|
|
25921
|
+
}
|
|
25922
|
+
if (!hasTags && !hasRelations) {
|
|
25923
|
+
legacy = true;
|
|
25924
|
+
legacyReasons.push("no graph presence");
|
|
25925
|
+
}
|
|
25926
|
+
let bucket;
|
|
25927
|
+
if (score < deleteBelow)
|
|
25928
|
+
bucket = "delete";
|
|
25929
|
+
else if (score < archiveBelow)
|
|
25930
|
+
bucket = "archive";
|
|
25931
|
+
else if (score < 70)
|
|
25932
|
+
bucket = "review";
|
|
25933
|
+
else
|
|
25934
|
+
bucket = "keep";
|
|
25935
|
+
const staleDraft = entity.memory_tier === "draft" && (entity.access_count || 0) === 0 && ageDays > staleDraftAgeDays;
|
|
25936
|
+
return {
|
|
25937
|
+
id: entity.id,
|
|
25938
|
+
title: entity.title,
|
|
25939
|
+
type: entity.type,
|
|
25940
|
+
tier: entity.memory_tier,
|
|
25941
|
+
ageDays: Math.round(ageDays),
|
|
25942
|
+
score,
|
|
25943
|
+
bucket,
|
|
25944
|
+
reasons,
|
|
25945
|
+
legacy,
|
|
25946
|
+
legacyReasons,
|
|
25947
|
+
staleDraft,
|
|
25948
|
+
subScores: {
|
|
25949
|
+
confidence: Math.round(confidence),
|
|
25950
|
+
decay: Math.round(decay),
|
|
25951
|
+
structural,
|
|
25952
|
+
content,
|
|
25953
|
+
tierAgeFit,
|
|
25954
|
+
access: Math.round(access)
|
|
25955
|
+
}
|
|
25956
|
+
};
|
|
25957
|
+
}
|
|
25958
|
+
async function runMemoryAudit(client3, workspaceId, projectId, options) {
|
|
25959
|
+
const dryRun = options?.dryRun !== false;
|
|
25960
|
+
const archiveBelow = options?.archiveBelow ?? 40;
|
|
25961
|
+
const deleteBelow = options?.deleteBelow ?? 20;
|
|
25962
|
+
const limit = options?.limit ?? 500;
|
|
25963
|
+
const staleDraftAgeDays = options?.staleDraftAgeDays ?? 7;
|
|
25964
|
+
const report = {
|
|
25965
|
+
success: true,
|
|
25966
|
+
dryRun,
|
|
25967
|
+
timestamp: new Date().toISOString(),
|
|
25968
|
+
workspace: { id: workspaceId, projectId },
|
|
25969
|
+
summary: {
|
|
25970
|
+
totalEntities: 0,
|
|
25971
|
+
scanned: 0,
|
|
25972
|
+
keep: 0,
|
|
25973
|
+
review: 0,
|
|
25974
|
+
archive: 0,
|
|
25975
|
+
delete: 0,
|
|
25976
|
+
legacyCount: 0,
|
|
25977
|
+
staleDraftCount: 0
|
|
25978
|
+
},
|
|
25979
|
+
actionsTaken: { flaggedReview: 0, archived: 0, deleted: 0 },
|
|
25980
|
+
distribution: { "0-20": 0, "20-40": 0, "40-70": 0, "70-100": 0 },
|
|
25981
|
+
legacyBreakdown: {
|
|
25982
|
+
defaultConfidence: 0,
|
|
25983
|
+
missingEmbedding: 0,
|
|
25984
|
+
stuckDraft: 0,
|
|
25985
|
+
noGraphPresence: 0
|
|
25986
|
+
},
|
|
25987
|
+
lowest: [],
|
|
25988
|
+
staleDrafts: [],
|
|
25989
|
+
errors: [],
|
|
25990
|
+
healthReport: ""
|
|
25991
|
+
};
|
|
25992
|
+
const entities = [];
|
|
25993
|
+
let offset = 0;
|
|
25994
|
+
try {
|
|
25995
|
+
while (entities.length < limit) {
|
|
25996
|
+
const pageSize = Math.min(BATCH_SIZE, limit - entities.length);
|
|
25997
|
+
const result = await client3.listMemoryEntities({
|
|
25998
|
+
workspace_id: workspaceId,
|
|
25999
|
+
project_id: projectId,
|
|
26000
|
+
limit: pageSize,
|
|
26001
|
+
offset
|
|
26002
|
+
});
|
|
26003
|
+
const page = result.entities || [];
|
|
26004
|
+
if (page.length === 0)
|
|
26005
|
+
break;
|
|
26006
|
+
entities.push(...page);
|
|
26007
|
+
if (page.length < pageSize)
|
|
26008
|
+
break;
|
|
26009
|
+
offset += pageSize;
|
|
26010
|
+
}
|
|
26011
|
+
} catch (err) {
|
|
26012
|
+
report.errors.push({
|
|
26013
|
+
step: "fetch",
|
|
26014
|
+
message: `Failed to fetch entities: ${err.message}`
|
|
26015
|
+
});
|
|
26016
|
+
report.success = false;
|
|
26017
|
+
report.healthReport = renderReport(report);
|
|
26018
|
+
return report;
|
|
26019
|
+
}
|
|
26020
|
+
report.summary.totalEntities = entities.length;
|
|
26021
|
+
const relationCounts = new Map;
|
|
26022
|
+
for (let i = 0;i < entities.length; i += CONCURRENCY_LIMIT) {
|
|
26023
|
+
const batch = entities.slice(i, i + CONCURRENCY_LIMIT);
|
|
26024
|
+
const results = await Promise.allSettled(batch.map(async (e) => {
|
|
26025
|
+
const related = await client3.getRelatedEntities(e.id);
|
|
26026
|
+
const count = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
|
|
26027
|
+
return { id: e.id, count };
|
|
26028
|
+
}));
|
|
26029
|
+
for (const r of results) {
|
|
26030
|
+
if (r.status === "fulfilled") {
|
|
26031
|
+
relationCounts.set(r.value.id, r.value.count);
|
|
26032
|
+
}
|
|
26033
|
+
}
|
|
26034
|
+
}
|
|
26035
|
+
const audits = [];
|
|
26036
|
+
for (const entity of entities) {
|
|
26037
|
+
const relCount = relationCounts.get(entity.id) ?? 0;
|
|
26038
|
+
const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow, staleDraftAgeDays);
|
|
26039
|
+
audits.push(audit);
|
|
26040
|
+
report.summary.scanned++;
|
|
26041
|
+
report.summary[audit.bucket]++;
|
|
26042
|
+
if (audit.legacy)
|
|
26043
|
+
report.summary.legacyCount++;
|
|
26044
|
+
if (audit.staleDraft)
|
|
26045
|
+
report.summary.staleDraftCount++;
|
|
26046
|
+
if (audit.score < 20)
|
|
26047
|
+
report.distribution["0-20"]++;
|
|
26048
|
+
else if (audit.score < 40)
|
|
26049
|
+
report.distribution["20-40"]++;
|
|
26050
|
+
else if (audit.score < 70)
|
|
26051
|
+
report.distribution["40-70"]++;
|
|
26052
|
+
else
|
|
26053
|
+
report.distribution["70-100"]++;
|
|
26054
|
+
for (const reason of audit.legacyReasons) {
|
|
26055
|
+
if (reason.startsWith("default confidence"))
|
|
26056
|
+
report.legacyBreakdown.defaultConfidence++;
|
|
26057
|
+
else if (reason.startsWith("pre-embeddings"))
|
|
26058
|
+
report.legacyBreakdown.missingEmbedding++;
|
|
26059
|
+
else if (reason.startsWith("stuck draft"))
|
|
26060
|
+
report.legacyBreakdown.stuckDraft++;
|
|
26061
|
+
else if (reason.startsWith("no graph"))
|
|
26062
|
+
report.legacyBreakdown.noGraphPresence++;
|
|
26063
|
+
}
|
|
26064
|
+
}
|
|
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);
|
|
26067
|
+
if (!dryRun) {
|
|
26068
|
+
for (const audit of audits) {
|
|
26069
|
+
try {
|
|
26070
|
+
if (audit.bucket === "delete") {
|
|
26071
|
+
await client3.deleteMemoryEntity(audit.id);
|
|
26072
|
+
report.actionsTaken.deleted++;
|
|
26073
|
+
} else if (audit.bucket === "archive") {
|
|
26074
|
+
await client3.updateMemoryEntity(audit.id, {
|
|
26075
|
+
confidence: 0.25,
|
|
26076
|
+
metadata: {
|
|
26077
|
+
audit_archived_at: new Date().toISOString(),
|
|
26078
|
+
audit_score: audit.score,
|
|
26079
|
+
audit_reasons: audit.reasons
|
|
26080
|
+
}
|
|
26081
|
+
});
|
|
26082
|
+
report.actionsTaken.archived++;
|
|
26083
|
+
} else if (audit.bucket === "review") {
|
|
26084
|
+
await client3.updateMemoryEntity(audit.id, {
|
|
26085
|
+
metadata: {
|
|
26086
|
+
needs_review: true,
|
|
26087
|
+
audit_score: audit.score,
|
|
26088
|
+
audit_reasons: audit.reasons,
|
|
26089
|
+
audit_at: new Date().toISOString()
|
|
26090
|
+
}
|
|
26091
|
+
});
|
|
26092
|
+
report.actionsTaken.flaggedReview++;
|
|
26093
|
+
}
|
|
26094
|
+
} catch (err) {
|
|
26095
|
+
report.errors.push({
|
|
26096
|
+
entityId: audit.id,
|
|
26097
|
+
step: audit.bucket,
|
|
26098
|
+
message: err.message
|
|
26099
|
+
});
|
|
26100
|
+
}
|
|
26101
|
+
}
|
|
26102
|
+
}
|
|
26103
|
+
report.healthReport = renderReport(report);
|
|
26104
|
+
return report;
|
|
26105
|
+
}
|
|
26106
|
+
function renderReport(report) {
|
|
26107
|
+
const mode = report.dryRun ? "Dry Run (preview)" : "Executed";
|
|
26108
|
+
const s = report.summary;
|
|
26109
|
+
const lines = [
|
|
26110
|
+
`# Memory Quality Audit
|
|
26111
|
+
`,
|
|
26112
|
+
`**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount} | **Stale drafts:** ${s.staleDraftCount}`,
|
|
26113
|
+
"",
|
|
26114
|
+
"## Distribution",
|
|
26115
|
+
`- 70-100 (keep): ${report.distribution["70-100"]}`,
|
|
26116
|
+
`- 40-69 (review): ${report.distribution["40-70"]}`,
|
|
26117
|
+
`- 20-39 (archive): ${report.distribution["20-40"]}`,
|
|
26118
|
+
`- 0-19 (delete): ${report.distribution["0-20"]}`,
|
|
26119
|
+
"",
|
|
26120
|
+
"## Buckets",
|
|
26121
|
+
`- **Keep:** ${s.keep}`,
|
|
26122
|
+
`- **Review:** ${s.review}${!report.dryRun ? ` (flagged ${report.actionsTaken.flaggedReview})` : ""}`,
|
|
26123
|
+
`- **Archive:** ${s.archive}${!report.dryRun ? ` (archived ${report.actionsTaken.archived})` : ""}`,
|
|
26124
|
+
`- **Delete:** ${s.delete}${!report.dryRun ? ` (deleted ${report.actionsTaken.deleted})` : ""}`,
|
|
26125
|
+
""
|
|
26126
|
+
];
|
|
26127
|
+
const l = report.legacyBreakdown;
|
|
26128
|
+
if (s.legacyCount > 0) {
|
|
26129
|
+
lines.push("## Legacy Breakdown");
|
|
26130
|
+
lines.push(`- Default confidence, never validated: ${l.defaultConfidence}`);
|
|
26131
|
+
lines.push(`- Pre-embeddings migration: ${l.missingEmbedding}`);
|
|
26132
|
+
lines.push(`- Stuck drafts (>60d, no promotion): ${l.stuckDraft}`);
|
|
26133
|
+
lines.push(`- No tags + no relations: ${l.noGraphPresence}`);
|
|
26134
|
+
lines.push("");
|
|
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
|
+
}
|
|
26147
|
+
if (report.lowest.length > 0) {
|
|
26148
|
+
lines.push("## Lowest-Scoring (top 10)");
|
|
26149
|
+
lines.push("| Score | Bucket | Tier | Age | Title | Reasons |");
|
|
26150
|
+
lines.push("|-------|--------|------|-----|-------|---------|");
|
|
26151
|
+
for (const a of report.lowest) {
|
|
26152
|
+
const reasonStr = a.reasons.slice(0, 3).join(", ") || "—";
|
|
26153
|
+
const titleTrunc = a.title.length > 40 ? `${a.title.slice(0, 37)}...` : a.title;
|
|
26154
|
+
lines.push(`| ${a.score} | ${a.bucket} | ${a.tier} | ${a.ageDays}d | ${titleTrunc} | ${reasonStr} |`);
|
|
26155
|
+
}
|
|
26156
|
+
lines.push("");
|
|
26157
|
+
}
|
|
26158
|
+
if (report.errors.length > 0) {
|
|
26159
|
+
lines.push("## Errors");
|
|
26160
|
+
for (const e of report.errors.slice(0, 10)) {
|
|
26161
|
+
lines.push(`- **${e.step}${e.entityId ? ` ${e.entityId}` : ""}:** ${e.message}`);
|
|
26162
|
+
}
|
|
26163
|
+
lines.push("");
|
|
26164
|
+
}
|
|
26165
|
+
if (report.dryRun) {
|
|
26166
|
+
lines.push("---");
|
|
26167
|
+
lines.push("*Run with `dryRun: false` to flag review entries, archive low-quality memories, and delete worst offenders.*");
|
|
26168
|
+
}
|
|
26169
|
+
return lines.join(`
|
|
26170
|
+
`);
|
|
26171
|
+
}
|
|
26172
|
+
|
|
25826
26173
|
// src/memory-cleanup.ts
|
|
25827
26174
|
init_dist();
|
|
25828
26175
|
var ALL_STEPS = [
|
|
@@ -25830,12 +26177,13 @@ var ALL_STEPS = [
|
|
|
25830
26177
|
"consolidate",
|
|
25831
26178
|
"orphans",
|
|
25832
26179
|
"duplicates",
|
|
25833
|
-
"backfill"
|
|
26180
|
+
"backfill",
|
|
26181
|
+
"audit"
|
|
25834
26182
|
];
|
|
25835
|
-
var
|
|
26183
|
+
var MS_PER_DAY2 = 1000 * 60 * 60 * 24;
|
|
25836
26184
|
var MAX_ENTITIES_FETCH = 200;
|
|
25837
26185
|
var DUPLICATE_SIMILARITY_THRESHOLD = 0.85;
|
|
25838
|
-
var
|
|
26186
|
+
var CONCURRENCY_LIMIT2 = 5;
|
|
25839
26187
|
async function runMemoryCleanup(client3, workspaceId, projectId, options) {
|
|
25840
26188
|
const dryRun = options?.dryRun !== false;
|
|
25841
26189
|
const steps = options?.steps ?? ALL_STEPS;
|
|
@@ -25991,6 +26339,44 @@ async function runMemoryCleanup(client3, workspaceId, projectId, options) {
|
|
|
25991
26339
|
});
|
|
25992
26340
|
}
|
|
25993
26341
|
}
|
|
26342
|
+
if (steps.includes("audit")) {
|
|
26343
|
+
try {
|
|
26344
|
+
const auditReport = await runMemoryAudit(client3, workspaceId, projectId, {
|
|
26345
|
+
dryRun,
|
|
26346
|
+
archiveBelow: options?.auditArchiveBelow,
|
|
26347
|
+
deleteBelow: options?.auditDeleteBelow
|
|
26348
|
+
});
|
|
26349
|
+
const low = auditReport.lowest.length > 0 ? auditReport.lowest[0].score : null;
|
|
26350
|
+
report.steps.audit = {
|
|
26351
|
+
scanned: auditReport.summary.scanned,
|
|
26352
|
+
legacyCount: auditReport.summary.legacyCount,
|
|
26353
|
+
buckets: {
|
|
26354
|
+
keep: auditReport.summary.keep,
|
|
26355
|
+
review: auditReport.summary.review,
|
|
26356
|
+
archive: auditReport.summary.archive,
|
|
26357
|
+
delete: auditReport.summary.delete
|
|
26358
|
+
},
|
|
26359
|
+
actions: auditReport.actionsTaken,
|
|
26360
|
+
lowestScore: low,
|
|
26361
|
+
report: auditReport
|
|
26362
|
+
};
|
|
26363
|
+
report.summary.issuesFound += auditReport.summary.review + auditReport.summary.archive + auditReport.summary.delete;
|
|
26364
|
+
if (!dryRun) {
|
|
26365
|
+
report.summary.actionsTaken += auditReport.actionsTaken.flaggedReview + auditReport.actionsTaken.archived + auditReport.actionsTaken.deleted;
|
|
26366
|
+
}
|
|
26367
|
+
for (const err of auditReport.errors) {
|
|
26368
|
+
report.errors.push({
|
|
26369
|
+
step: `audit:${err.step}`,
|
|
26370
|
+
message: err.entityId ? `${err.entityId}: ${err.message}` : err.message
|
|
26371
|
+
});
|
|
26372
|
+
}
|
|
26373
|
+
} catch (err) {
|
|
26374
|
+
report.errors.push({
|
|
26375
|
+
step: "audit",
|
|
26376
|
+
message: err.message
|
|
26377
|
+
});
|
|
26378
|
+
}
|
|
26379
|
+
}
|
|
25994
26380
|
report.healthReport = generateHealthReport(report);
|
|
25995
26381
|
return report;
|
|
25996
26382
|
}
|
|
@@ -25999,7 +26385,7 @@ function runPruneStep(entities, maxAgeDays) {
|
|
|
25999
26385
|
const drafts = entities.filter((e) => e.memory_tier === "draft");
|
|
26000
26386
|
const stale = [];
|
|
26001
26387
|
for (const entity of drafts) {
|
|
26002
|
-
const ageDays = (now - new Date(entity.created_at).getTime()) /
|
|
26388
|
+
const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY2;
|
|
26003
26389
|
if (ageDays < maxAgeDays)
|
|
26004
26390
|
continue;
|
|
26005
26391
|
const lifecycle2 = evaluateLifecycle(entity);
|
|
@@ -26020,17 +26406,17 @@ async function runOrphanStep(client3, entities, orphanAgeDays) {
|
|
|
26020
26406
|
return false;
|
|
26021
26407
|
if (e.access_count >= 2)
|
|
26022
26408
|
return false;
|
|
26023
|
-
const ageDays = (now - new Date(e.created_at).getTime()) /
|
|
26409
|
+
const ageDays = (now - new Date(e.created_at).getTime()) / MS_PER_DAY2;
|
|
26024
26410
|
return ageDays >= orphanAgeDays;
|
|
26025
26411
|
});
|
|
26026
|
-
for (let i = 0;i < candidates.length; i +=
|
|
26027
|
-
const batch = candidates.slice(i, i +
|
|
26412
|
+
for (let i = 0;i < candidates.length; i += CONCURRENCY_LIMIT2) {
|
|
26413
|
+
const batch = candidates.slice(i, i + CONCURRENCY_LIMIT2);
|
|
26028
26414
|
const results = await Promise.allSettled(batch.map(async (entity) => {
|
|
26029
26415
|
const related = await client3.getRelatedEntities(entity.id);
|
|
26030
26416
|
const totalRelations = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
|
|
26031
26417
|
if (totalRelations > 0)
|
|
26032
26418
|
return null;
|
|
26033
|
-
const ageDays = (now - new Date(entity.created_at).getTime()) /
|
|
26419
|
+
const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY2;
|
|
26034
26420
|
return {
|
|
26035
26421
|
id: entity.id,
|
|
26036
26422
|
title: entity.title,
|
|
@@ -26059,8 +26445,8 @@ async function runDuplicateStep(client3, entities, workspaceId, projectId) {
|
|
|
26059
26445
|
const flaggedForRemoval = new Set;
|
|
26060
26446
|
const entityMap = new Map(entities.map((e) => [e.id, e]));
|
|
26061
26447
|
const similarityMap = new Map;
|
|
26062
|
-
for (let i = 0;i < entities.length; i +=
|
|
26063
|
-
const batch = entities.slice(i, i +
|
|
26448
|
+
for (let i = 0;i < entities.length; i += CONCURRENCY_LIMIT2) {
|
|
26449
|
+
const batch = entities.slice(i, i + CONCURRENCY_LIMIT2);
|
|
26064
26450
|
const results = await Promise.allSettled(batch.map(async (entity) => {
|
|
26065
26451
|
const similar = await findSimilarEntities(client3, entity.title, entity.content, workspaceId, { projectId, limit: 5, minRrfScore: 0.05, excludeIds: [entity.id] });
|
|
26066
26452
|
return { entityId: entity.id, similar };
|
|
@@ -26212,6 +26598,20 @@ function generateHealthReport(report) {
|
|
|
26212
26598
|
`);
|
|
26213
26599
|
}
|
|
26214
26600
|
}
|
|
26601
|
+
if (report.steps.audit) {
|
|
26602
|
+
const a = report.steps.audit;
|
|
26603
|
+
lines.push("## Quality Audit");
|
|
26604
|
+
lines.push(`Scanned ${a.scanned} entities. Legacy signals on ${a.legacyCount}.`);
|
|
26605
|
+
lines.push(`Buckets — keep: ${a.buckets.keep}, review: ${a.buckets.review}, archive: ${a.buckets.archive}, delete: ${a.buckets.delete}.`);
|
|
26606
|
+
if (!report.dryRun) {
|
|
26607
|
+
lines.push(`Actions — flagged: ${a.actions.flaggedReview}, archived: ${a.actions.archived}, deleted: ${a.actions.deleted}.`);
|
|
26608
|
+
}
|
|
26609
|
+
if (a.report.lowest.length > 0) {
|
|
26610
|
+
const worst = a.report.lowest[0];
|
|
26611
|
+
lines.push(`Lowest score: **${worst.score}** — "${worst.title}" (${worst.reasons.slice(0, 2).join(", ") || "—"}).`);
|
|
26612
|
+
}
|
|
26613
|
+
lines.push("");
|
|
26614
|
+
}
|
|
26215
26615
|
if (report.errors.length > 0) {
|
|
26216
26616
|
lines.push("## Errors");
|
|
26217
26617
|
for (const e of report.errors) {
|
|
@@ -26256,7 +26656,7 @@ async function purgeMemories(client3, workspaceId, projectId, options) {
|
|
|
26256
26656
|
continue;
|
|
26257
26657
|
if (filters.olderThanDays !== undefined) {
|
|
26258
26658
|
const ref = entity.last_accessed_at || entity.created_at;
|
|
26259
|
-
const ageDays = (now - new Date(ref).getTime()) /
|
|
26659
|
+
const ageDays = (now - new Date(ref).getTime()) / MS_PER_DAY2;
|
|
26260
26660
|
if (ageDays < filters.olderThanDays)
|
|
26261
26661
|
continue;
|
|
26262
26662
|
}
|
|
@@ -26272,13 +26672,13 @@ async function purgeMemories(client3, workspaceId, projectId, options) {
|
|
|
26272
26672
|
type: e.type,
|
|
26273
26673
|
tier: e.memory_tier,
|
|
26274
26674
|
confidence: e.confidence,
|
|
26275
|
-
ageDays: Math.round((now - new Date(e.last_accessed_at || e.created_at).getTime()) /
|
|
26675
|
+
ageDays: Math.round((now - new Date(e.last_accessed_at || e.created_at).getTime()) / MS_PER_DAY2)
|
|
26276
26676
|
}));
|
|
26277
26677
|
const errors3 = [];
|
|
26278
26678
|
let purged = 0;
|
|
26279
26679
|
if (!dryRun) {
|
|
26280
|
-
for (let i = 0;i < allMatches.length; i +=
|
|
26281
|
-
const batch = allMatches.slice(i, i +
|
|
26680
|
+
for (let i = 0;i < allMatches.length; i += CONCURRENCY_LIMIT2) {
|
|
26681
|
+
const batch = allMatches.slice(i, i + CONCURRENCY_LIMIT2);
|
|
26282
26682
|
const results = await Promise.allSettled(batch.map((e) => client3.deleteMemoryEntity(e.id)));
|
|
26283
26683
|
for (let j = 0;j < results.length; j++) {
|
|
26284
26684
|
if (results[j].status === "fulfilled") {
|
|
@@ -27755,7 +28155,7 @@ var TOOLS = {
|
|
|
27755
28155
|
}
|
|
27756
28156
|
},
|
|
27757
28157
|
harmony_cleanup_memories: {
|
|
27758
|
-
description: "Run a unified memory cleanup: prune stale drafts, consolidate similar memories, detect orphans and duplicates, and
|
|
28158
|
+
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.",
|
|
27759
28159
|
inputSchema: {
|
|
27760
28160
|
type: "object",
|
|
27761
28161
|
properties: {
|
|
@@ -27775,9 +28175,16 @@ var TOOLS = {
|
|
|
27775
28175
|
type: "array",
|
|
27776
28176
|
items: {
|
|
27777
28177
|
type: "string",
|
|
27778
|
-
enum: [
|
|
28178
|
+
enum: [
|
|
28179
|
+
"prune",
|
|
28180
|
+
"consolidate",
|
|
28181
|
+
"orphans",
|
|
28182
|
+
"duplicates",
|
|
28183
|
+
"backfill",
|
|
28184
|
+
"audit"
|
|
28185
|
+
]
|
|
27779
28186
|
},
|
|
27780
|
-
description: "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill."
|
|
28187
|
+
description: "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill, audit."
|
|
27781
28188
|
},
|
|
27782
28189
|
maxAgeDays: {
|
|
27783
28190
|
type: "number",
|
|
@@ -27790,6 +28197,51 @@ var TOOLS = {
|
|
|
27790
28197
|
orphanAgeDays: {
|
|
27791
28198
|
type: "number",
|
|
27792
28199
|
description: "Min age in days for orphan detection (default: 14)"
|
|
28200
|
+
},
|
|
28201
|
+
auditArchiveBelow: {
|
|
28202
|
+
type: "number",
|
|
28203
|
+
description: "Audit: archive entities scoring below this (default: 40)"
|
|
28204
|
+
},
|
|
28205
|
+
auditDeleteBelow: {
|
|
28206
|
+
type: "number",
|
|
28207
|
+
description: "Audit: delete entities scoring below this (default: 20)"
|
|
28208
|
+
}
|
|
28209
|
+
},
|
|
28210
|
+
required: []
|
|
28211
|
+
}
|
|
28212
|
+
},
|
|
28213
|
+
harmony_audit_memories: {
|
|
28214
|
+
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.",
|
|
28215
|
+
inputSchema: {
|
|
28216
|
+
type: "object",
|
|
28217
|
+
properties: {
|
|
28218
|
+
workspaceId: {
|
|
28219
|
+
type: "string",
|
|
28220
|
+
description: "Workspace ID (optional if context set)"
|
|
28221
|
+
},
|
|
28222
|
+
projectId: {
|
|
28223
|
+
type: "string",
|
|
28224
|
+
description: "Project ID (optional)"
|
|
28225
|
+
},
|
|
28226
|
+
dryRun: {
|
|
28227
|
+
type: "boolean",
|
|
28228
|
+
description: "Preview audit without flagging/archiving/deleting (default: true)"
|
|
28229
|
+
},
|
|
28230
|
+
archiveBelow: {
|
|
28231
|
+
type: "number",
|
|
28232
|
+
description: "Score threshold below which entities are archived (confidence set to 0.25). Default: 40"
|
|
28233
|
+
},
|
|
28234
|
+
deleteBelow: {
|
|
28235
|
+
type: "number",
|
|
28236
|
+
description: "Score threshold below which entities are hard-deleted. Default: 20. Set to 0 to never delete."
|
|
28237
|
+
},
|
|
28238
|
+
limit: {
|
|
28239
|
+
type: "number",
|
|
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."
|
|
27793
28245
|
}
|
|
27794
28246
|
},
|
|
27795
28247
|
required: []
|
|
@@ -29359,6 +29811,32 @@ async function handleToolCall(name, args, deps) {
|
|
|
29359
29811
|
message: `Processed ${entitiesProcessed} entities, created ${relationsCreated} new relations.`
|
|
29360
29812
|
};
|
|
29361
29813
|
}
|
|
29814
|
+
case "harmony_audit_memories": {
|
|
29815
|
+
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
|
29816
|
+
if (!workspaceId) {
|
|
29817
|
+
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
29818
|
+
}
|
|
29819
|
+
const projectId = args.projectId || deps.getActiveProjectId() || undefined;
|
|
29820
|
+
const report = await runMemoryAudit(client3, workspaceId, projectId, {
|
|
29821
|
+
dryRun: args.dryRun,
|
|
29822
|
+
archiveBelow: args.archiveBelow,
|
|
29823
|
+
deleteBelow: args.deleteBelow,
|
|
29824
|
+
limit: args.limit,
|
|
29825
|
+
staleDraftAgeDays: args.staleDraftAgeDays
|
|
29826
|
+
});
|
|
29827
|
+
return {
|
|
29828
|
+
success: report.success,
|
|
29829
|
+
dryRun: report.dryRun,
|
|
29830
|
+
summary: report.summary,
|
|
29831
|
+
distribution: report.distribution,
|
|
29832
|
+
legacyBreakdown: report.legacyBreakdown,
|
|
29833
|
+
actionsTaken: report.actionsTaken,
|
|
29834
|
+
lowest: report.lowest,
|
|
29835
|
+
staleDrafts: report.staleDrafts,
|
|
29836
|
+
errors: report.errors,
|
|
29837
|
+
healthReport: report.healthReport
|
|
29838
|
+
};
|
|
29839
|
+
}
|
|
29362
29840
|
case "harmony_cleanup_memories": {
|
|
29363
29841
|
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
|
29364
29842
|
if (!workspaceId) {
|
|
@@ -29370,7 +29848,8 @@ async function handleToolCall(name, args, deps) {
|
|
|
29370
29848
|
"consolidate",
|
|
29371
29849
|
"orphans",
|
|
29372
29850
|
"duplicates",
|
|
29373
|
-
"backfill"
|
|
29851
|
+
"backfill",
|
|
29852
|
+
"audit"
|
|
29374
29853
|
];
|
|
29375
29854
|
const rawSteps = args.steps;
|
|
29376
29855
|
const steps = rawSteps?.filter((s) => validSteps.includes(s));
|
|
@@ -29383,7 +29862,9 @@ async function handleToolCall(name, args, deps) {
|
|
|
29383
29862
|
steps,
|
|
29384
29863
|
maxAgeDays: args.maxAgeDays,
|
|
29385
29864
|
minClusterSize: args.minClusterSize,
|
|
29386
|
-
orphanAgeDays: args.orphanAgeDays
|
|
29865
|
+
orphanAgeDays: args.orphanAgeDays,
|
|
29866
|
+
auditArchiveBelow: args.auditArchiveBelow,
|
|
29867
|
+
auditDeleteBelow: args.auditDeleteBelow
|
|
29387
29868
|
});
|
|
29388
29869
|
return {
|
|
29389
29870
|
success: report.success,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"serve:remote": "bun src/remote.ts",
|
|
60
60
|
"dev": "bun --watch src/index.ts",
|
|
61
61
|
"test": "bun run test:unit && bun run test:integration",
|
|
62
|
-
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts",
|
|
62
|
+
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts",
|
|
63
63
|
"test:integration": "bun test src/__tests__/integration-memory-system.test.ts src/__tests__/integration-memory-crud.test.ts",
|
|
64
64
|
"typecheck": "tsc --noEmit",
|
|
65
65
|
"prepublishOnly": "bun run build"
|