@gethmy/mcp 2.3.3 → 2.4.0
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 +670 -19
- package/dist/index.js +670 -19
- package/package.json +2 -2
- package/src/__tests__/memory-audit.test.ts +296 -0
- package/src/api-client.ts +6 -0
- package/src/memory-audit.ts +485 -0
- package/src/memory-cleanup.ts +247 -1
- package/src/server.ts +237 -11
package/src/memory-cleanup.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
consolidateMemories,
|
|
16
16
|
} from "./consolidation.js";
|
|
17
17
|
import { findSimilarEntities } from "./graph-expansion.js";
|
|
18
|
+
import { type AuditReport, runMemoryAudit } from "./memory-audit.js";
|
|
18
19
|
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Types
|
|
@@ -39,7 +40,8 @@ export type CleanupStep =
|
|
|
39
40
|
| "consolidate"
|
|
40
41
|
| "orphans"
|
|
41
42
|
| "duplicates"
|
|
42
|
-
| "backfill"
|
|
43
|
+
| "backfill"
|
|
44
|
+
| "audit";
|
|
43
45
|
|
|
44
46
|
export interface CleanupOptions {
|
|
45
47
|
dryRun?: boolean;
|
|
@@ -47,6 +49,45 @@ export interface CleanupOptions {
|
|
|
47
49
|
maxAgeDays?: number;
|
|
48
50
|
minClusterSize?: number;
|
|
49
51
|
orphanAgeDays?: number;
|
|
52
|
+
auditArchiveBelow?: number;
|
|
53
|
+
auditDeleteBelow?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Purge types
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
export interface PurgeFilters {
|
|
61
|
+
tier?: "draft" | "episode" | "reference";
|
|
62
|
+
scope?: string;
|
|
63
|
+
type?: string;
|
|
64
|
+
olderThanDays?: number;
|
|
65
|
+
maxConfidence?: number;
|
|
66
|
+
tags?: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PurgeOptions {
|
|
70
|
+
dryRun?: boolean;
|
|
71
|
+
filters: PurgeFilters;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PurgeReport {
|
|
75
|
+
success: boolean;
|
|
76
|
+
dryRun: boolean;
|
|
77
|
+
timestamp: string;
|
|
78
|
+
workspace: { id: string; projectId: string };
|
|
79
|
+
filters: PurgeFilters;
|
|
80
|
+
matched: number;
|
|
81
|
+
purged: number;
|
|
82
|
+
items: Array<{
|
|
83
|
+
id: string;
|
|
84
|
+
title: string;
|
|
85
|
+
type: string;
|
|
86
|
+
tier: string;
|
|
87
|
+
confidence: number;
|
|
88
|
+
ageDays: number;
|
|
89
|
+
}>;
|
|
90
|
+
errors: Array<{ entityId: string; message: string }>;
|
|
50
91
|
}
|
|
51
92
|
|
|
52
93
|
interface PruneStepResult {
|
|
@@ -98,6 +139,15 @@ interface BackfillStepResult {
|
|
|
98
139
|
errors: Array<{ entity_id: string; error: string }>;
|
|
99
140
|
}
|
|
100
141
|
|
|
142
|
+
interface AuditStepResult {
|
|
143
|
+
scanned: number;
|
|
144
|
+
legacyCount: number;
|
|
145
|
+
buckets: { keep: number; review: number; archive: number; delete: number };
|
|
146
|
+
actions: { flaggedReview: number; archived: number; deleted: number };
|
|
147
|
+
lowestScore: number | null;
|
|
148
|
+
report: AuditReport;
|
|
149
|
+
}
|
|
150
|
+
|
|
101
151
|
export interface CleanupReport {
|
|
102
152
|
success: boolean;
|
|
103
153
|
dryRun: boolean;
|
|
@@ -116,6 +166,7 @@ export interface CleanupReport {
|
|
|
116
166
|
orphans?: OrphanStepResult;
|
|
117
167
|
duplicates?: DuplicateStepResult;
|
|
118
168
|
backfill?: BackfillStepResult;
|
|
169
|
+
audit?: AuditStepResult;
|
|
119
170
|
};
|
|
120
171
|
|
|
121
172
|
errors: Array<{ step: string; message: string }>;
|
|
@@ -128,6 +179,7 @@ const ALL_STEPS: CleanupStep[] = [
|
|
|
128
179
|
"orphans",
|
|
129
180
|
"duplicates",
|
|
130
181
|
"backfill",
|
|
182
|
+
"audit",
|
|
131
183
|
];
|
|
132
184
|
|
|
133
185
|
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
@@ -322,6 +374,55 @@ export async function runMemoryCleanup(
|
|
|
322
374
|
}
|
|
323
375
|
}
|
|
324
376
|
|
|
377
|
+
// Stage 6: Quality audit — rate every entity against modern standards
|
|
378
|
+
if (steps.includes("audit")) {
|
|
379
|
+
try {
|
|
380
|
+
const auditReport = await runMemoryAudit(client, workspaceId, projectId, {
|
|
381
|
+
dryRun,
|
|
382
|
+
archiveBelow: options?.auditArchiveBelow,
|
|
383
|
+
deleteBelow: options?.auditDeleteBelow,
|
|
384
|
+
});
|
|
385
|
+
const low =
|
|
386
|
+
auditReport.lowest.length > 0 ? auditReport.lowest[0].score : null;
|
|
387
|
+
report.steps.audit = {
|
|
388
|
+
scanned: auditReport.summary.scanned,
|
|
389
|
+
legacyCount: auditReport.summary.legacyCount,
|
|
390
|
+
buckets: {
|
|
391
|
+
keep: auditReport.summary.keep,
|
|
392
|
+
review: auditReport.summary.review,
|
|
393
|
+
archive: auditReport.summary.archive,
|
|
394
|
+
delete: auditReport.summary.delete,
|
|
395
|
+
},
|
|
396
|
+
actions: auditReport.actionsTaken,
|
|
397
|
+
lowestScore: low,
|
|
398
|
+
report: auditReport,
|
|
399
|
+
};
|
|
400
|
+
report.summary.issuesFound +=
|
|
401
|
+
auditReport.summary.review +
|
|
402
|
+
auditReport.summary.archive +
|
|
403
|
+
auditReport.summary.delete;
|
|
404
|
+
if (!dryRun) {
|
|
405
|
+
report.summary.actionsTaken +=
|
|
406
|
+
auditReport.actionsTaken.flaggedReview +
|
|
407
|
+
auditReport.actionsTaken.archived +
|
|
408
|
+
auditReport.actionsTaken.deleted;
|
|
409
|
+
}
|
|
410
|
+
for (const err of auditReport.errors) {
|
|
411
|
+
report.errors.push({
|
|
412
|
+
step: `audit:${err.step}`,
|
|
413
|
+
message: err.entityId
|
|
414
|
+
? `${err.entityId}: ${err.message}`
|
|
415
|
+
: err.message,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
} catch (err) {
|
|
419
|
+
report.errors.push({
|
|
420
|
+
step: "audit",
|
|
421
|
+
message: (err as Error).message,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
325
426
|
report.healthReport = generateHealthReport(report);
|
|
326
427
|
return report;
|
|
327
428
|
}
|
|
@@ -639,6 +740,30 @@ function generateHealthReport(report: CleanupReport): string {
|
|
|
639
740
|
}
|
|
640
741
|
}
|
|
641
742
|
|
|
743
|
+
// Audit
|
|
744
|
+
if (report.steps.audit) {
|
|
745
|
+
const a = report.steps.audit;
|
|
746
|
+
lines.push("## Quality Audit");
|
|
747
|
+
lines.push(
|
|
748
|
+
`Scanned ${a.scanned} entities. Legacy signals on ${a.legacyCount}.`,
|
|
749
|
+
);
|
|
750
|
+
lines.push(
|
|
751
|
+
`Buckets — keep: ${a.buckets.keep}, review: ${a.buckets.review}, archive: ${a.buckets.archive}, delete: ${a.buckets.delete}.`,
|
|
752
|
+
);
|
|
753
|
+
if (!report.dryRun) {
|
|
754
|
+
lines.push(
|
|
755
|
+
`Actions — flagged: ${a.actions.flaggedReview}, archived: ${a.actions.archived}, deleted: ${a.actions.deleted}.`,
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
if (a.report.lowest.length > 0) {
|
|
759
|
+
const worst = a.report.lowest[0];
|
|
760
|
+
lines.push(
|
|
761
|
+
`Lowest score: **${worst.score}** — "${worst.title}" (${worst.reasons.slice(0, 2).join(", ") || "—"}).`,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
lines.push("");
|
|
765
|
+
}
|
|
766
|
+
|
|
642
767
|
// Errors
|
|
643
768
|
if (report.errors.length > 0) {
|
|
644
769
|
lines.push("## Errors");
|
|
@@ -654,3 +779,124 @@ function generateHealthReport(report: CleanupReport): string {
|
|
|
654
779
|
|
|
655
780
|
return lines.join("\n");
|
|
656
781
|
}
|
|
782
|
+
|
|
783
|
+
// ---------------------------------------------------------------------------
|
|
784
|
+
// Purge — filtered bulk deletion
|
|
785
|
+
// ---------------------------------------------------------------------------
|
|
786
|
+
|
|
787
|
+
export async function purgeMemories(
|
|
788
|
+
client: HarmonyApiClient,
|
|
789
|
+
workspaceId: string,
|
|
790
|
+
projectId: string,
|
|
791
|
+
options: PurgeOptions,
|
|
792
|
+
): Promise<PurgeReport> {
|
|
793
|
+
const dryRun = options.dryRun !== false;
|
|
794
|
+
const { filters } = options;
|
|
795
|
+
|
|
796
|
+
// Safety: require at least one narrowing filter
|
|
797
|
+
const hasFilter =
|
|
798
|
+
filters.tier ||
|
|
799
|
+
filters.scope ||
|
|
800
|
+
filters.type ||
|
|
801
|
+
filters.olderThanDays !== undefined ||
|
|
802
|
+
filters.maxConfidence !== undefined ||
|
|
803
|
+
(filters.tags && filters.tags.length > 0);
|
|
804
|
+
|
|
805
|
+
if (!hasFilter) {
|
|
806
|
+
throw new Error(
|
|
807
|
+
"At least one narrowing filter (tier, scope, type, olderThanDays, maxConfidence, tags) is required.",
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Paginate through all matching entities
|
|
812
|
+
const allMatches: MemoryEntity[] = [];
|
|
813
|
+
let offset = 0;
|
|
814
|
+
const pageSize = 100;
|
|
815
|
+
const now = Date.now();
|
|
816
|
+
|
|
817
|
+
while (true) {
|
|
818
|
+
const result = await client.listMemoryEntities({
|
|
819
|
+
workspace_id: workspaceId,
|
|
820
|
+
project_id: projectId,
|
|
821
|
+
type: filters.type,
|
|
822
|
+
scope: filters.scope,
|
|
823
|
+
tags: filters.tags,
|
|
824
|
+
limit: pageSize,
|
|
825
|
+
offset,
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
const entities = (result.entities || []) as MemoryEntity[];
|
|
829
|
+
if (entities.length === 0) break;
|
|
830
|
+
|
|
831
|
+
// Client-side filtering for fields the API doesn't support natively
|
|
832
|
+
for (const entity of entities) {
|
|
833
|
+
if (filters.tier && entity.memory_tier !== filters.tier) continue;
|
|
834
|
+
if (
|
|
835
|
+
filters.maxConfidence !== undefined &&
|
|
836
|
+
entity.confidence > filters.maxConfidence
|
|
837
|
+
)
|
|
838
|
+
continue;
|
|
839
|
+
if (filters.olderThanDays !== undefined) {
|
|
840
|
+
const ref = entity.last_accessed_at || entity.created_at;
|
|
841
|
+
const ageDays = (now - new Date(ref).getTime()) / MS_PER_DAY;
|
|
842
|
+
if (ageDays < filters.olderThanDays) continue;
|
|
843
|
+
}
|
|
844
|
+
allMatches.push(entity);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (entities.length < pageSize) break;
|
|
848
|
+
offset += pageSize;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Build preview items
|
|
852
|
+
const items = allMatches.map((e) => ({
|
|
853
|
+
id: e.id,
|
|
854
|
+
title: e.title,
|
|
855
|
+
type: e.type,
|
|
856
|
+
tier: e.memory_tier,
|
|
857
|
+
confidence: e.confidence,
|
|
858
|
+
ageDays: Math.round(
|
|
859
|
+
(now - new Date(e.last_accessed_at || e.created_at).getTime()) /
|
|
860
|
+
MS_PER_DAY,
|
|
861
|
+
),
|
|
862
|
+
}));
|
|
863
|
+
|
|
864
|
+
// Execute deletions if not dry-run
|
|
865
|
+
const errors: Array<{ entityId: string; message: string }> = [];
|
|
866
|
+
let purged = 0;
|
|
867
|
+
|
|
868
|
+
if (!dryRun) {
|
|
869
|
+
// Delete in batches to avoid overwhelming the API
|
|
870
|
+
for (let i = 0; i < allMatches.length; i += CONCURRENCY_LIMIT) {
|
|
871
|
+
const batch = allMatches.slice(i, i + CONCURRENCY_LIMIT);
|
|
872
|
+
const results = await Promise.allSettled(
|
|
873
|
+
batch.map((e) => client.deleteMemoryEntity(e.id)),
|
|
874
|
+
);
|
|
875
|
+
for (let j = 0; j < results.length; j++) {
|
|
876
|
+
if (results[j].status === "fulfilled") {
|
|
877
|
+
purged++;
|
|
878
|
+
} else {
|
|
879
|
+
errors.push({
|
|
880
|
+
entityId: batch[j].id,
|
|
881
|
+
message:
|
|
882
|
+
results[j].status === "rejected"
|
|
883
|
+
? String((results[j] as PromiseRejectedResult).reason)
|
|
884
|
+
: "Unknown error",
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return {
|
|
892
|
+
success: errors.length === 0,
|
|
893
|
+
dryRun,
|
|
894
|
+
timestamp: new Date().toISOString(),
|
|
895
|
+
workspace: { id: workspaceId, projectId },
|
|
896
|
+
filters,
|
|
897
|
+
matched: allMatches.length,
|
|
898
|
+
purged: dryRun ? 0 : purged,
|
|
899
|
+
items,
|
|
900
|
+
errors,
|
|
901
|
+
};
|
|
902
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -58,7 +58,12 @@ import {
|
|
|
58
58
|
} from "./context-assembly.js";
|
|
59
59
|
import { autoExpandGraph } from "./graph-expansion.js";
|
|
60
60
|
import { runLifecycleMaintenance } from "./lifecycle-maintenance.js";
|
|
61
|
-
import {
|
|
61
|
+
import { runMemoryAudit } from "./memory-audit.js";
|
|
62
|
+
import {
|
|
63
|
+
type PurgeFilters,
|
|
64
|
+
purgeMemories,
|
|
65
|
+
runMemoryCleanup,
|
|
66
|
+
} from "./memory-cleanup.js";
|
|
62
67
|
import { onboardNewUser } from "./onboard.js";
|
|
63
68
|
import type { PromptVariant } from "./prompt-builder.js";
|
|
64
69
|
|
|
@@ -721,6 +726,22 @@ const TOOLS = {
|
|
|
721
726
|
type: "number",
|
|
722
727
|
description: "Updated time estimate",
|
|
723
728
|
},
|
|
729
|
+
actions: {
|
|
730
|
+
type: "array",
|
|
731
|
+
items: {
|
|
732
|
+
type: "object",
|
|
733
|
+
properties: {
|
|
734
|
+
description: {
|
|
735
|
+
type: "string",
|
|
736
|
+
description:
|
|
737
|
+
"What was done, e.g. 'Edited CardDetailSheet.tsx — added done toggle'",
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
required: ["description"],
|
|
741
|
+
},
|
|
742
|
+
description:
|
|
743
|
+
"Actions performed since last update. Each becomes a visible activity log entry.",
|
|
744
|
+
},
|
|
724
745
|
},
|
|
725
746
|
required: ["cardId", "agentIdentifier", "agentName"],
|
|
726
747
|
},
|
|
@@ -1641,7 +1662,7 @@ const TOOLS = {
|
|
|
1641
1662
|
|
|
1642
1663
|
harmony_cleanup_memories: {
|
|
1643
1664
|
description:
|
|
1644
|
-
"Run a unified memory cleanup: prune stale drafts, consolidate similar memories, detect orphans and duplicates, and
|
|
1665
|
+
"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.",
|
|
1645
1666
|
inputSchema: {
|
|
1646
1667
|
type: "object",
|
|
1647
1668
|
properties: {
|
|
@@ -1662,10 +1683,17 @@ const TOOLS = {
|
|
|
1662
1683
|
type: "array",
|
|
1663
1684
|
items: {
|
|
1664
1685
|
type: "string",
|
|
1665
|
-
enum: [
|
|
1686
|
+
enum: [
|
|
1687
|
+
"prune",
|
|
1688
|
+
"consolidate",
|
|
1689
|
+
"orphans",
|
|
1690
|
+
"duplicates",
|
|
1691
|
+
"backfill",
|
|
1692
|
+
"audit",
|
|
1693
|
+
],
|
|
1666
1694
|
},
|
|
1667
1695
|
description:
|
|
1668
|
-
"Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill.",
|
|
1696
|
+
"Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill, audit.",
|
|
1669
1697
|
},
|
|
1670
1698
|
maxAgeDays: {
|
|
1671
1699
|
type: "number",
|
|
@@ -1679,6 +1707,106 @@ const TOOLS = {
|
|
|
1679
1707
|
type: "number",
|
|
1680
1708
|
description: "Min age in days for orphan detection (default: 14)",
|
|
1681
1709
|
},
|
|
1710
|
+
auditArchiveBelow: {
|
|
1711
|
+
type: "number",
|
|
1712
|
+
description: "Audit: archive entities scoring below this (default: 40)",
|
|
1713
|
+
},
|
|
1714
|
+
auditDeleteBelow: {
|
|
1715
|
+
type: "number",
|
|
1716
|
+
description: "Audit: delete entities scoring below this (default: 20)",
|
|
1717
|
+
},
|
|
1718
|
+
},
|
|
1719
|
+
required: [],
|
|
1720
|
+
},
|
|
1721
|
+
},
|
|
1722
|
+
harmony_audit_memories: {
|
|
1723
|
+
description:
|
|
1724
|
+
"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.",
|
|
1725
|
+
inputSchema: {
|
|
1726
|
+
type: "object",
|
|
1727
|
+
properties: {
|
|
1728
|
+
workspaceId: {
|
|
1729
|
+
type: "string",
|
|
1730
|
+
description: "Workspace ID (optional if context set)",
|
|
1731
|
+
},
|
|
1732
|
+
projectId: {
|
|
1733
|
+
type: "string",
|
|
1734
|
+
description: "Project ID (optional)",
|
|
1735
|
+
},
|
|
1736
|
+
dryRun: {
|
|
1737
|
+
type: "boolean",
|
|
1738
|
+
description:
|
|
1739
|
+
"Preview audit without flagging/archiving/deleting (default: true)",
|
|
1740
|
+
},
|
|
1741
|
+
archiveBelow: {
|
|
1742
|
+
type: "number",
|
|
1743
|
+
description:
|
|
1744
|
+
"Score threshold below which entities are archived (confidence set to 0.25). Default: 40",
|
|
1745
|
+
},
|
|
1746
|
+
deleteBelow: {
|
|
1747
|
+
type: "number",
|
|
1748
|
+
description:
|
|
1749
|
+
"Score threshold below which entities are hard-deleted. Default: 20. Set to 0 to never delete.",
|
|
1750
|
+
},
|
|
1751
|
+
limit: {
|
|
1752
|
+
type: "number",
|
|
1753
|
+
description:
|
|
1754
|
+
"Max number of entities to audit (default: 500). Paginated fetch.",
|
|
1755
|
+
},
|
|
1756
|
+
},
|
|
1757
|
+
required: [],
|
|
1758
|
+
},
|
|
1759
|
+
},
|
|
1760
|
+
harmony_purge_memories: {
|
|
1761
|
+
description:
|
|
1762
|
+
"Bulk-delete memory entities matching filters within a project. Requires at least one narrowing filter (tier, scope, type, olderThanDays, maxConfidence, tags). Dry-run by default — preview what would be deleted before executing.",
|
|
1763
|
+
inputSchema: {
|
|
1764
|
+
type: "object",
|
|
1765
|
+
properties: {
|
|
1766
|
+
workspaceId: {
|
|
1767
|
+
type: "string",
|
|
1768
|
+
description: "Workspace ID (optional if context set)",
|
|
1769
|
+
},
|
|
1770
|
+
projectId: {
|
|
1771
|
+
type: "string",
|
|
1772
|
+
description:
|
|
1773
|
+
"Project ID (required — purge is project-scoped). Falls back to active project context.",
|
|
1774
|
+
},
|
|
1775
|
+
dryRun: {
|
|
1776
|
+
type: "boolean",
|
|
1777
|
+
description:
|
|
1778
|
+
"Preview what would be deleted without executing (default: true)",
|
|
1779
|
+
},
|
|
1780
|
+
tier: {
|
|
1781
|
+
type: "string",
|
|
1782
|
+
enum: ["draft", "episode", "reference"],
|
|
1783
|
+
description: 'Filter by memory tier (e.g. "draft")',
|
|
1784
|
+
},
|
|
1785
|
+
scope: {
|
|
1786
|
+
type: "string",
|
|
1787
|
+
description:
|
|
1788
|
+
'Filter by scope (e.g. "private", "project", "workspace")',
|
|
1789
|
+
},
|
|
1790
|
+
type: {
|
|
1791
|
+
type: "string",
|
|
1792
|
+
description:
|
|
1793
|
+
'Filter by entity type (e.g. "error", "pattern", "lesson", "decision")',
|
|
1794
|
+
},
|
|
1795
|
+
olderThanDays: {
|
|
1796
|
+
type: "number",
|
|
1797
|
+
description:
|
|
1798
|
+
"Only include entities not accessed in at least this many days",
|
|
1799
|
+
},
|
|
1800
|
+
maxConfidence: {
|
|
1801
|
+
type: "number",
|
|
1802
|
+
description:
|
|
1803
|
+
"Only include entities with confidence at or below this value (e.g. 0.3 for low-confidence junk)",
|
|
1804
|
+
},
|
|
1805
|
+
tags: {
|
|
1806
|
+
type: "array",
|
|
1807
|
+
items: { type: "string" },
|
|
1808
|
+
description: "Only include entities matching these tags",
|
|
1809
|
+
},
|
|
1682
1810
|
},
|
|
1683
1811
|
required: [],
|
|
1684
1812
|
},
|
|
@@ -2643,18 +2771,26 @@ async function handleToolCall(
|
|
|
2643
2771
|
args.progressPercent !== undefined
|
|
2644
2772
|
? z.number().min(0).max(100).parse(args.progressPercent)
|
|
2645
2773
|
: undefined;
|
|
2646
|
-
//
|
|
2647
|
-
const
|
|
2648
|
-
| {
|
|
2774
|
+
// Convert actions parameter to recentActions format and merge with memory actions
|
|
2775
|
+
const callerActions = args.actions as
|
|
2776
|
+
| { description: string }[]
|
|
2649
2777
|
| undefined;
|
|
2778
|
+
const now = new Date().toISOString();
|
|
2779
|
+
const callerRecentActions: { action: string; ts: string }[] = [
|
|
2780
|
+
...((args.recentActions as { action: string; ts: string }[]) || []),
|
|
2781
|
+
...(callerActions || []).map((a) => ({
|
|
2782
|
+
action: a.description,
|
|
2783
|
+
ts: now,
|
|
2784
|
+
})),
|
|
2785
|
+
];
|
|
2650
2786
|
const memSession = getMemorySession(cardId);
|
|
2651
2787
|
let mergedRecentActions: { action: string; ts: string }[] | undefined;
|
|
2652
2788
|
if (memSession?.dirty) {
|
|
2653
2789
|
mergedRecentActions = mergeMemoryActionsInto(
|
|
2654
2790
|
cardId,
|
|
2655
|
-
callerRecentActions
|
|
2791
|
+
callerRecentActions,
|
|
2656
2792
|
);
|
|
2657
|
-
} else if (callerRecentActions) {
|
|
2793
|
+
} else if (callerRecentActions.length > 0) {
|
|
2658
2794
|
mergedRecentActions = callerRecentActions;
|
|
2659
2795
|
}
|
|
2660
2796
|
|
|
@@ -4013,6 +4149,37 @@ async function handleToolCall(
|
|
|
4013
4149
|
};
|
|
4014
4150
|
}
|
|
4015
4151
|
|
|
4152
|
+
case "harmony_audit_memories": {
|
|
4153
|
+
const workspaceId =
|
|
4154
|
+
(args.workspaceId as string) || deps.getActiveWorkspaceId();
|
|
4155
|
+
if (!workspaceId) {
|
|
4156
|
+
throw new Error(
|
|
4157
|
+
"No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
|
|
4158
|
+
);
|
|
4159
|
+
}
|
|
4160
|
+
const projectId =
|
|
4161
|
+
(args.projectId as string) || deps.getActiveProjectId() || undefined;
|
|
4162
|
+
|
|
4163
|
+
const report = await runMemoryAudit(client, workspaceId, projectId, {
|
|
4164
|
+
dryRun: args.dryRun as boolean | undefined,
|
|
4165
|
+
archiveBelow: args.archiveBelow as number | undefined,
|
|
4166
|
+
deleteBelow: args.deleteBelow as number | undefined,
|
|
4167
|
+
limit: args.limit as number | undefined,
|
|
4168
|
+
});
|
|
4169
|
+
|
|
4170
|
+
return {
|
|
4171
|
+
success: report.success,
|
|
4172
|
+
dryRun: report.dryRun,
|
|
4173
|
+
summary: report.summary,
|
|
4174
|
+
distribution: report.distribution,
|
|
4175
|
+
legacyBreakdown: report.legacyBreakdown,
|
|
4176
|
+
actionsTaken: report.actionsTaken,
|
|
4177
|
+
lowest: report.lowest,
|
|
4178
|
+
errors: report.errors,
|
|
4179
|
+
healthReport: report.healthReport,
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4016
4183
|
case "harmony_cleanup_memories": {
|
|
4017
4184
|
const workspaceId =
|
|
4018
4185
|
(args.workspaceId as string) || deps.getActiveWorkspaceId();
|
|
@@ -4030,6 +4197,7 @@ async function handleToolCall(
|
|
|
4030
4197
|
"orphans",
|
|
4031
4198
|
"duplicates",
|
|
4032
4199
|
"backfill",
|
|
4200
|
+
"audit",
|
|
4033
4201
|
];
|
|
4034
4202
|
const rawSteps = args.steps as string[] | undefined;
|
|
4035
4203
|
const steps = rawSteps?.filter((s) => validSteps.includes(s));
|
|
@@ -4041,10 +4209,14 @@ async function handleToolCall(
|
|
|
4041
4209
|
|
|
4042
4210
|
const report = await runMemoryCleanup(client, workspaceId, projectId, {
|
|
4043
4211
|
dryRun: args.dryRun as boolean | undefined,
|
|
4044
|
-
steps
|
|
4212
|
+
steps: steps as
|
|
4213
|
+
| ("prune" | "consolidate" | "orphans" | "duplicates" | "backfill" | "audit")[]
|
|
4214
|
+
| undefined,
|
|
4045
4215
|
maxAgeDays: args.maxAgeDays as number | undefined,
|
|
4046
4216
|
minClusterSize: args.minClusterSize as number | undefined,
|
|
4047
4217
|
orphanAgeDays: args.orphanAgeDays as number | undefined,
|
|
4218
|
+
auditArchiveBelow: args.auditArchiveBelow as number | undefined,
|
|
4219
|
+
auditDeleteBelow: args.auditDeleteBelow as number | undefined,
|
|
4048
4220
|
});
|
|
4049
4221
|
|
|
4050
4222
|
return {
|
|
@@ -4056,6 +4228,49 @@ async function handleToolCall(
|
|
|
4056
4228
|
};
|
|
4057
4229
|
}
|
|
4058
4230
|
|
|
4231
|
+
case "harmony_purge_memories": {
|
|
4232
|
+
const workspaceId =
|
|
4233
|
+
(args.workspaceId as string) || deps.getActiveWorkspaceId();
|
|
4234
|
+
if (!workspaceId) {
|
|
4235
|
+
throw new Error(
|
|
4236
|
+
"No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
|
|
4237
|
+
);
|
|
4238
|
+
}
|
|
4239
|
+
const projectId = (args.projectId as string) || deps.getActiveProjectId();
|
|
4240
|
+
if (!projectId) {
|
|
4241
|
+
throw new Error(
|
|
4242
|
+
"No project specified. Purge requires a project scope. Use harmony_set_project_context or provide projectId.",
|
|
4243
|
+
);
|
|
4244
|
+
}
|
|
4245
|
+
|
|
4246
|
+
const filters: PurgeFilters = {};
|
|
4247
|
+
if (args.tier) filters.tier = args.tier as PurgeFilters["tier"];
|
|
4248
|
+
if (args.scope) filters.scope = args.scope as string;
|
|
4249
|
+
if (args.type) filters.type = args.type as string;
|
|
4250
|
+
if (args.olderThanDays !== undefined)
|
|
4251
|
+
filters.olderThanDays = args.olderThanDays as number;
|
|
4252
|
+
if (args.maxConfidence !== undefined)
|
|
4253
|
+
filters.maxConfidence = args.maxConfidence as number;
|
|
4254
|
+
if (args.tags) filters.tags = args.tags as string[];
|
|
4255
|
+
|
|
4256
|
+
const report = await purgeMemories(client, workspaceId, projectId, {
|
|
4257
|
+
dryRun: args.dryRun as boolean | undefined,
|
|
4258
|
+
filters,
|
|
4259
|
+
});
|
|
4260
|
+
|
|
4261
|
+
return {
|
|
4262
|
+
success: report.success,
|
|
4263
|
+
dryRun: report.dryRun,
|
|
4264
|
+
matched: report.matched,
|
|
4265
|
+
purged: report.purged,
|
|
4266
|
+
items: report.items,
|
|
4267
|
+
errors: report.errors,
|
|
4268
|
+
message: report.dryRun
|
|
4269
|
+
? `Found ${report.matched} entities matching filters. Run with dryRun=false to delete.`
|
|
4270
|
+
: `Purged ${report.purged} of ${report.matched} matching entities.`,
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4059
4274
|
default:
|
|
4060
4275
|
throw new Error(`Unknown tool: ${name}`);
|
|
4061
4276
|
}
|
|
@@ -4109,6 +4324,7 @@ export class HarmonyMCPServer {
|
|
|
4109
4324
|
);
|
|
4110
4325
|
|
|
4111
4326
|
// Graceful shutdown: end all auto-sessions
|
|
4327
|
+
let exitCode = 0;
|
|
4112
4328
|
const handleShutdown = async () => {
|
|
4113
4329
|
try {
|
|
4114
4330
|
await shutdownAllSessions();
|
|
@@ -4116,10 +4332,20 @@ export class HarmonyMCPServer {
|
|
|
4116
4332
|
// Best-effort
|
|
4117
4333
|
}
|
|
4118
4334
|
destroyAutoSession();
|
|
4119
|
-
process.exit(
|
|
4335
|
+
process.exit(exitCode);
|
|
4120
4336
|
};
|
|
4121
4337
|
process.on("SIGINT", handleShutdown);
|
|
4122
4338
|
process.on("SIGTERM", handleShutdown);
|
|
4339
|
+
process.on("uncaughtException", (err) => {
|
|
4340
|
+
console.error("MCP server uncaught exception:", err);
|
|
4341
|
+
exitCode = 1;
|
|
4342
|
+
handleShutdown();
|
|
4343
|
+
});
|
|
4344
|
+
process.on("unhandledRejection", (reason) => {
|
|
4345
|
+
console.error("MCP server unhandled rejection:", reason);
|
|
4346
|
+
exitCode = 1;
|
|
4347
|
+
handleShutdown();
|
|
4348
|
+
});
|
|
4123
4349
|
|
|
4124
4350
|
// Attempt startup sync (non-blocking, best-effort)
|
|
4125
4351
|
try {
|