@gethmy/mcp 2.2.4 → 2.3.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 +580 -335
- package/dist/index.js +580 -335
- package/dist/lib/active-learning.js +73 -129
- package/dist/lib/consolidation.js +71 -11
- package/dist/lib/context-assembly.js +69 -4
- package/dist/lib/memory-cleanup.js +426 -0
- package/dist/lib/prompt-builder.js +5 -1
- package/dist/lib/server.js +63 -0
- package/package.json +1 -1
- package/src/active-learning.ts +83 -145
- package/src/consolidation.ts +81 -12
- package/src/context-assembly.ts +75 -4
- package/src/memory-cleanup.ts +616 -0
- package/src/prompt-builder.ts +13 -1
- package/src/server.ts +74 -0
package/dist/cli.js
CHANGED
|
@@ -9171,6 +9171,29 @@ __export(exports_context_assembly, {
|
|
|
9171
9171
|
function estimateTokens(text) {
|
|
9172
9172
|
return Math.ceil(text.length / 4);
|
|
9173
9173
|
}
|
|
9174
|
+
function passesQualityGate(entity) {
|
|
9175
|
+
const content = entity.content.trim();
|
|
9176
|
+
if (content.length < 50)
|
|
9177
|
+
return false;
|
|
9178
|
+
const normalizedTitle = entity.title.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim();
|
|
9179
|
+
const normalizedContent = content.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim();
|
|
9180
|
+
if (normalizedContent.length < normalizedTitle.length * 1.5) {
|
|
9181
|
+
return false;
|
|
9182
|
+
}
|
|
9183
|
+
if (entity.type === "pattern" && /recurring .+ \(\d+ instances\)/i.test(entity.title)) {
|
|
9184
|
+
const lines = content.split(`
|
|
9185
|
+
`).filter((l) => l.trim().length > 0);
|
|
9186
|
+
const bulletLines = lines.filter((l) => l.trim().startsWith("- "));
|
|
9187
|
+
if (bulletLines.length > lines.length * 0.6)
|
|
9188
|
+
return false;
|
|
9189
|
+
}
|
|
9190
|
+
if (entity.type === "procedure") {
|
|
9191
|
+
const stepCount = (content.match(/^\d+\.\s/gm) || []).length;
|
|
9192
|
+
if (stepCount < 3)
|
|
9193
|
+
return false;
|
|
9194
|
+
}
|
|
9195
|
+
return true;
|
|
9196
|
+
}
|
|
9174
9197
|
function generateAssemblyId() {
|
|
9175
9198
|
return `ctx_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
9176
9199
|
}
|
|
@@ -9430,7 +9453,27 @@ async function assembleContext(options) {
|
|
|
9430
9453
|
memories: []
|
|
9431
9454
|
};
|
|
9432
9455
|
}
|
|
9433
|
-
const
|
|
9456
|
+
const qualityCandidates = candidates.filter((entity) => {
|
|
9457
|
+
if (passesQualityGate(entity))
|
|
9458
|
+
return true;
|
|
9459
|
+
manifest.excluded.push({
|
|
9460
|
+
entityId: entity.id,
|
|
9461
|
+
title: entity.title,
|
|
9462
|
+
type: entity.type,
|
|
9463
|
+
tier: entity.memory_tier,
|
|
9464
|
+
relevanceScore: 0,
|
|
9465
|
+
reason: "failed_quality_gate"
|
|
9466
|
+
});
|
|
9467
|
+
return false;
|
|
9468
|
+
});
|
|
9469
|
+
if (qualityCandidates.length === 0) {
|
|
9470
|
+
return {
|
|
9471
|
+
context: "",
|
|
9472
|
+
manifest,
|
|
9473
|
+
memories: []
|
|
9474
|
+
};
|
|
9475
|
+
}
|
|
9476
|
+
const scored = qualityCandidates.map((entity) => {
|
|
9434
9477
|
const { score, reasons } = computeRelevanceScore(entity, taskContext, cardLabels, graphRelations.length > 0 ? graphRelations : undefined);
|
|
9435
9478
|
return { entity, score, reasons };
|
|
9436
9479
|
});
|
|
@@ -9767,7 +9810,7 @@ async function recordContextFeedback(client2, cardId, sessionStatus, progressPer
|
|
|
9767
9810
|
sessionAssemblyMap.delete(cardId);
|
|
9768
9811
|
return { adjusted };
|
|
9769
9812
|
}
|
|
9770
|
-
var DEFAULT_TOKEN_BUDGET = 4000, MAX_TOKENS_PER_ENTITY = 500, MIN_RELEVANCE_THRESHOLD = 0.
|
|
9813
|
+
var DEFAULT_TOKEN_BUDGET = 4000, MAX_TOKENS_PER_ENTITY = 500, MIN_RELEVANCE_THRESHOLD = 0.15, TIER_WEIGHTS, PROCEDURE_BUDGET_FRACTION = 0.15, TIER_BUDGET_ALLOCATION, MIN_REFERENCE_SLOTS = 1, GRAPH_WALK_MAX_DEPTH = 1, GRAPH_WALK_MAX_ENTITIES = 10, GRAPH_WALK_MIN_CONFIDENCE = 0.5, GRAPH_WALK_SEED_COUNT = 5, MAX_QUERY_VARIATIONS = 4, RERANK_CLUSTER_THRESHOLD = 0.05, RERANK_TOP_N = 10, RERANK_MIN_CANDIDATES = 5, RELATION_BONUSES, QUERY_SYNONYMS, manifestCache, MAX_CACHE_SIZE = 50, sessionAssemblyMap, MAX_SESSION_MAP_SIZE = 100;
|
|
9771
9814
|
var init_context_assembly = __esm(() => {
|
|
9772
9815
|
init_dist();
|
|
9773
9816
|
TIER_WEIGHTS = {
|
|
@@ -9935,7 +9978,11 @@ ${card.description}`);
|
|
|
9935
9978
|
roleFraming.focus.forEach((f) => {
|
|
9936
9979
|
sections.push(`- ${f}`);
|
|
9937
9980
|
});
|
|
9938
|
-
sections.push(`- **Memory:**
|
|
9981
|
+
sections.push(`- **Memory:** Store reusable knowledge via \`harmony_remember\`. Only store what a future agent couldn't easily discover from the code itself, applies beyond this specific card, and includes a "because" (not just what, but why).`);
|
|
9982
|
+
sections.push(` - GOOD: "BoardContext card state must use moveCard action, never direct setState — optimistic updates depend on action ordering"`);
|
|
9983
|
+
sections.push(` - GOOD: "Mobile bottom bar is 64px, overlaps fixed-position drawers — always add pb-16 to drawer content"`);
|
|
9984
|
+
sections.push(` - BAD: "Fixed the login button" (no reusable knowledge — the fix is in the code)`);
|
|
9985
|
+
sections.push(` - BAD: "Completed card #42" (ephemeral, auto-tracked by session)`);
|
|
9939
9986
|
sections.push(`
|
|
9940
9987
|
## Suggested Outputs`);
|
|
9941
9988
|
roleFraming.outputSuggestions.forEach((s) => {
|
|
@@ -26531,13 +26578,13 @@ function levenshteinSimilarity(a, b) {
|
|
|
26531
26578
|
const maxLen = Math.max(sa.length, sb.length);
|
|
26532
26579
|
return 1 - matrix[sa.length][sb.length] / maxLen;
|
|
26533
26580
|
}
|
|
26534
|
-
async function extractMidSessionLearnings(
|
|
26581
|
+
async function extractMidSessionLearnings(_client, ctx) {
|
|
26535
26582
|
const workspaceId = getActiveWorkspaceId();
|
|
26536
26583
|
if (!workspaceId)
|
|
26537
26584
|
return { count: 0, entityIds: [] };
|
|
26538
|
-
const
|
|
26585
|
+
const _projectId = getActiveProjectId() || undefined;
|
|
26539
26586
|
const now = Date.now();
|
|
26540
|
-
const
|
|
26587
|
+
const _entityIds = [];
|
|
26541
26588
|
const history = sessionTaskHistory.get(ctx.cardId);
|
|
26542
26589
|
if (ctx.currentTask) {
|
|
26543
26590
|
const previousTask = history?.lastTask || "";
|
|
@@ -26570,81 +26617,22 @@ async function extractMidSessionLearnings(client2, ctx) {
|
|
|
26570
26617
|
}
|
|
26571
26618
|
}
|
|
26572
26619
|
if (ctx.status === "blocked" && ctx.blockers?.length) {
|
|
26573
|
-
for (const blocker of ctx.blockers) {
|
|
26574
|
-
try {
|
|
26575
|
-
const result = await client2.createMemoryEntity({
|
|
26576
|
-
workspace_id: workspaceId,
|
|
26577
|
-
project_id: projectId,
|
|
26578
|
-
type: "error",
|
|
26579
|
-
scope: "project",
|
|
26580
|
-
memory_tier: "draft",
|
|
26581
|
-
title: `Blocker (mid-session): ${blocker.slice(0, 100)}`,
|
|
26582
|
-
content: `Encountered while working on "${ctx.cardTitle}":
|
|
26583
|
-
|
|
26584
|
-
${blocker}
|
|
26585
|
-
|
|
26586
|
-
Agent: ${ctx.agentName}
|
|
26587
|
-
Progress: ${ctx.progressPercent ?? "unknown"}%`,
|
|
26588
|
-
confidence: 0.5,
|
|
26589
|
-
tags: ["auto-extracted", "blocker", "mid-session"],
|
|
26590
|
-
metadata: {
|
|
26591
|
-
source: "mid_session",
|
|
26592
|
-
card_id: ctx.cardId
|
|
26593
|
-
},
|
|
26594
|
-
agent_identifier: ctx.agentIdentifier
|
|
26595
|
-
});
|
|
26596
|
-
const entity = result.entity;
|
|
26597
|
-
if (entity?.id)
|
|
26598
|
-
entityIds.push(entity.id);
|
|
26599
|
-
} catch {}
|
|
26600
|
-
}
|
|
26601
26620
|
sessionTaskHistory.set(ctx.cardId, {
|
|
26602
26621
|
lastTask: ctx.currentTask || "",
|
|
26603
26622
|
lastExtractionAt: now,
|
|
26604
26623
|
steps: history?.steps || []
|
|
26605
26624
|
});
|
|
26606
|
-
return { count:
|
|
26625
|
+
return { count: 0, entityIds: [] };
|
|
26607
26626
|
}
|
|
26608
26627
|
if (ctx.currentTask) {
|
|
26609
|
-
const previousTask = history?.lastTask || "";
|
|
26610
|
-
const similarity = levenshteinSimilarity(previousTask, ctx.currentTask);
|
|
26611
|
-
if (similarity < 0.6 && previousTask.length > 0) {
|
|
26612
|
-
try {
|
|
26613
|
-
const result = await client2.createMemoryEntity({
|
|
26614
|
-
workspace_id: workspaceId,
|
|
26615
|
-
project_id: projectId,
|
|
26616
|
-
type: "context",
|
|
26617
|
-
scope: "project",
|
|
26618
|
-
memory_tier: "draft",
|
|
26619
|
-
title: `Task transition: ${ctx.cardTitle}`,
|
|
26620
|
-
content: `Agent transitioned tasks on "${ctx.cardTitle}".
|
|
26621
|
-
|
|
26622
|
-
Previous: ${previousTask}
|
|
26623
|
-
Current: ${ctx.currentTask}
|
|
26624
|
-
Progress: ${ctx.progressPercent ?? "unknown"}%`,
|
|
26625
|
-
confidence: 0.5,
|
|
26626
|
-
tags: ["auto-extracted", "task-transition", "mid-session"],
|
|
26627
|
-
metadata: {
|
|
26628
|
-
source: "mid_session",
|
|
26629
|
-
card_id: ctx.cardId,
|
|
26630
|
-
previous_task: previousTask,
|
|
26631
|
-
current_task: ctx.currentTask
|
|
26632
|
-
},
|
|
26633
|
-
agent_identifier: ctx.agentIdentifier
|
|
26634
|
-
});
|
|
26635
|
-
const entity = result.entity;
|
|
26636
|
-
if (entity?.id)
|
|
26637
|
-
entityIds.push(entity.id);
|
|
26638
|
-
} catch {}
|
|
26639
|
-
}
|
|
26640
26628
|
const currentHistory = sessionTaskHistory.get(ctx.cardId);
|
|
26641
26629
|
sessionTaskHistory.set(ctx.cardId, {
|
|
26642
26630
|
lastTask: ctx.currentTask,
|
|
26643
|
-
lastExtractionAt:
|
|
26631
|
+
lastExtractionAt: currentHistory?.lastExtractionAt ?? 0,
|
|
26644
26632
|
steps: currentHistory?.steps || []
|
|
26645
26633
|
});
|
|
26646
26634
|
}
|
|
26647
|
-
return { count:
|
|
26635
|
+
return { count: 0, entityIds: [] };
|
|
26648
26636
|
}
|
|
26649
26637
|
function clearMidSessionTracking(cardId) {
|
|
26650
26638
|
sessionTaskHistory.delete(cardId);
|
|
@@ -26828,49 +26816,60 @@ async function extractLearnings(client2, session) {
|
|
|
26828
26816
|
Related: ${relatedEntityTitles.map((t) => `[[${t}]]`).join(", ")}` : "";
|
|
26829
26817
|
if (session.blockers && session.blockers.length > 0) {
|
|
26830
26818
|
for (const blocker of session.blockers) {
|
|
26831
|
-
|
|
26832
|
-
|
|
26833
|
-
|
|
26819
|
+
if (blocker.length < 80)
|
|
26820
|
+
continue;
|
|
26821
|
+
let isDuplicate = false;
|
|
26822
|
+
try {
|
|
26823
|
+
const similar = await findSimilarEntities(client2, blocker.slice(0, 200), blocker, workspaceId, { projectId, limit: 3, minRrfScore: 0.05 });
|
|
26824
|
+
isDuplicate = similar.some((e) => e.type === "error" && (e.rrf_score ?? 0) >= 0.06);
|
|
26825
|
+
} catch {}
|
|
26826
|
+
if (!isDuplicate) {
|
|
26827
|
+
learnings.push({
|
|
26828
|
+
title: `Blocker: ${blocker.slice(0, 100)}`,
|
|
26829
|
+
content: `Encountered while working on "${session.cardTitle}":
|
|
26834
26830
|
|
|
26835
26831
|
${blocker}
|
|
26836
26832
|
|
|
26837
26833
|
Agent: ${session.agentName}
|
|
26838
26834
|
Session status: ${session.status}`,
|
|
26839
|
-
|
|
26840
|
-
|
|
26841
|
-
|
|
26842
|
-
|
|
26843
|
-
|
|
26844
|
-
|
|
26845
|
-
|
|
26846
|
-
|
|
26847
|
-
|
|
26835
|
+
type: "error",
|
|
26836
|
+
tier: "episode",
|
|
26837
|
+
confidence: 0.6,
|
|
26838
|
+
tags: [
|
|
26839
|
+
"auto-extracted",
|
|
26840
|
+
"blocker",
|
|
26841
|
+
...session.cardLabels.slice(0, 3)
|
|
26842
|
+
],
|
|
26843
|
+
metadata: {
|
|
26844
|
+
source: "active_learning",
|
|
26845
|
+
card_id: session.cardId
|
|
26846
|
+
}
|
|
26847
|
+
});
|
|
26848
|
+
}
|
|
26848
26849
|
}
|
|
26849
26850
|
}
|
|
26850
|
-
|
|
26851
|
-
if (session.status === "completed" && hasMeaningfulContent) {
|
|
26851
|
+
if (session.status === "paused" && (session.blockers?.length ?? 0) > 0) {
|
|
26852
26852
|
const durationInfo = session.sessionDurationMs ? `
|
|
26853
26853
|
Duration: ${Math.round(session.sessionDurationMs / 60000)} minutes` : "";
|
|
26854
26854
|
learnings.push({
|
|
26855
|
-
title: `
|
|
26855
|
+
title: `Paused: ${session.cardTitle}`,
|
|
26856
26856
|
content: [
|
|
26857
|
-
`
|
|
26858
|
-
session.currentTask ? `
|
|
26857
|
+
`Paused work on "${session.cardTitle}".`,
|
|
26858
|
+
session.currentTask ? `Last task: ${session.currentTask}` : "",
|
|
26859
26859
|
session.progressPercent !== undefined ? `Progress: ${session.progressPercent}%` : "",
|
|
26860
26860
|
durationInfo,
|
|
26861
|
-
session.
|
|
26862
|
-
session.blockers?.length ? `Blockers encountered: ${session.blockers.join("; ")}` : "",
|
|
26861
|
+
session.blockers?.length ? `Blockers: ${session.blockers.join("; ")}` : "",
|
|
26863
26862
|
`
|
|
26864
26863
|
Agent: ${session.agentName}`,
|
|
26865
26864
|
wikiLinksLine
|
|
26866
26865
|
].filter(Boolean).join(`
|
|
26867
26866
|
`),
|
|
26868
26867
|
type: "lesson",
|
|
26869
|
-
tier: "
|
|
26870
|
-
confidence: 0.
|
|
26868
|
+
tier: "draft",
|
|
26869
|
+
confidence: 0.6,
|
|
26871
26870
|
tags: [
|
|
26872
26871
|
"auto-extracted",
|
|
26873
|
-
"session-
|
|
26872
|
+
"session-paused",
|
|
26874
26873
|
...session.cardLabels.slice(0, 3)
|
|
26875
26874
|
],
|
|
26876
26875
|
metadata: {
|
|
@@ -26879,35 +26878,14 @@ Agent: ${session.agentName}`,
|
|
|
26879
26878
|
}
|
|
26880
26879
|
});
|
|
26881
26880
|
}
|
|
26882
|
-
const hasBugLabel = session.cardLabels.some((l) => ["bug", "fix", "hotfix", "defect", "error"].includes(l.toLowerCase()));
|
|
26883
|
-
if (hasBugLabel && session.status === "completed") {
|
|
26884
|
-
learnings.push({
|
|
26885
|
-
title: `Solution: ${session.cardTitle}`,
|
|
26886
|
-
content: [
|
|
26887
|
-
`Resolved bug: "${session.cardTitle}"`,
|
|
26888
|
-
session.currentTask ? `
|
|
26889
|
-
Approach: ${session.currentTask}` : "",
|
|
26890
|
-
`
|
|
26891
|
-
Agent: ${session.agentName}`,
|
|
26892
|
-
wikiLinksLine
|
|
26893
|
-
].filter(Boolean).join(`
|
|
26894
|
-
`),
|
|
26895
|
-
type: "solution",
|
|
26896
|
-
tier: "reference",
|
|
26897
|
-
confidence: 0.8,
|
|
26898
|
-
tags: ["auto-extracted", "bug-fix", ...session.cardLabels.slice(0, 3)],
|
|
26899
|
-
metadata: {
|
|
26900
|
-
source: "active_learning",
|
|
26901
|
-
card_id: session.cardId,
|
|
26902
|
-
auto_confidence: true
|
|
26903
|
-
}
|
|
26904
|
-
});
|
|
26905
|
-
}
|
|
26906
26881
|
const entityIds = [];
|
|
26907
26882
|
const stepHistory = sessionTaskHistory.get(session.cardId);
|
|
26908
|
-
const
|
|
26883
|
+
const MIN_PROCEDURE_STEPS = 5;
|
|
26884
|
+
const MIN_PROCEDURE_DURATION_MS = 10 * 60 * 1000;
|
|
26885
|
+
const hasEnoughSteps = stepHistory && stepHistory.steps.length >= MIN_PROCEDURE_STEPS;
|
|
26886
|
+
const hasMinDuration = (session.sessionDurationMs ?? 0) >= MIN_PROCEDURE_DURATION_MS;
|
|
26909
26887
|
const isSuccessful = session.status === "completed" && (session.progressPercent === undefined || session.progressPercent >= 85) && !session.blockers?.length;
|
|
26910
|
-
if (isSuccessful && hasEnoughSteps) {
|
|
26888
|
+
if (isSuccessful && hasEnoughSteps && hasMinDuration) {
|
|
26911
26889
|
const procedureResult = await extractOrReinforceProcedure(client2, session, stepHistory.steps, workspaceId, projectId, wikiLinksLine);
|
|
26912
26890
|
if (procedureResult) {
|
|
26913
26891
|
if (procedureResult.mode === "created") {
|
|
@@ -26951,218 +26929,9 @@ Agent: ${session.agentName}`,
|
|
|
26951
26929
|
if (createdPairs.length >= 2) {
|
|
26952
26930
|
linkSessionEntities(client2, createdPairs, workspaceId, projectId).catch(() => {});
|
|
26953
26931
|
}
|
|
26954
|
-
if (entityIds.length > 0) {
|
|
26955
|
-
detectAndCreatePatterns(client2, entityIds, session, workspaceId, projectId).catch(() => {});
|
|
26956
|
-
}
|
|
26957
|
-
if (createdPairs.length > 0) {
|
|
26958
|
-
detectCausalPatterns(client2, createdPairs, session, workspaceId, projectId).catch(() => {});
|
|
26959
|
-
}
|
|
26960
26932
|
clearMidSessionTracking(session.cardId);
|
|
26961
26933
|
return { count: entityIds.length, entityIds };
|
|
26962
26934
|
}
|
|
26963
|
-
var PATTERN_THRESHOLD = 3;
|
|
26964
|
-
async function detectAndCreatePatterns(client2, newEntityIds, session, workspaceId, projectId) {
|
|
26965
|
-
const patternEntityIds = [];
|
|
26966
|
-
for (const newEntityId of newEntityIds) {
|
|
26967
|
-
try {
|
|
26968
|
-
const { entity: rawEntity } = await client2.getMemoryEntity(newEntityId);
|
|
26969
|
-
const entity = rawEntity;
|
|
26970
|
-
if (!entity?.type)
|
|
26971
|
-
continue;
|
|
26972
|
-
const similar = await findSimilarEntities(client2, entity.title, entity.content, workspaceId, { projectId, limit: 30, minRrfScore: 0.01 });
|
|
26973
|
-
const existing = similar.filter((c) => !newEntityIds.includes(c.id) && c.type === entity.type);
|
|
26974
|
-
if (existing.length < PATTERN_THRESHOLD)
|
|
26975
|
-
continue;
|
|
26976
|
-
const memberTitles = [
|
|
26977
|
-
entity.title,
|
|
26978
|
-
...existing.slice(0, 4).map((e) => e.title)
|
|
26979
|
-
];
|
|
26980
|
-
const patternTitle = `Pattern: recurring ${entity.type} (${existing.length + 1} instances)`;
|
|
26981
|
-
const { entities: existingPatterns } = await client2.listMemoryEntities({
|
|
26982
|
-
workspace_id: workspaceId,
|
|
26983
|
-
project_id: projectId,
|
|
26984
|
-
type: "pattern",
|
|
26985
|
-
limit: 10
|
|
26986
|
-
});
|
|
26987
|
-
const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_type === entity.type);
|
|
26988
|
-
let patternId = null;
|
|
26989
|
-
if (matchingPattern) {
|
|
26990
|
-
patternId = matchingPattern.id;
|
|
26991
|
-
await client2.updateMemoryEntity(patternId, {
|
|
26992
|
-
content: `Recurring pattern: ${entity.type} entities appearing ${existing.length + 1} times.
|
|
26993
|
-
|
|
26994
|
-
Members:
|
|
26995
|
-
${memberTitles.map((t) => `- ${t}`).join(`
|
|
26996
|
-
`)}
|
|
26997
|
-
|
|
26998
|
-
Last updated: ${new Date().toISOString()}`,
|
|
26999
|
-
metadata: {
|
|
27000
|
-
pattern_count: existing.length + 1,
|
|
27001
|
-
pattern_type: entity.type,
|
|
27002
|
-
last_updated: new Date().toISOString()
|
|
27003
|
-
}
|
|
27004
|
-
});
|
|
27005
|
-
} else {
|
|
27006
|
-
const result = await client2.createMemoryEntity({
|
|
27007
|
-
workspace_id: workspaceId,
|
|
27008
|
-
project_id: projectId,
|
|
27009
|
-
type: "pattern",
|
|
27010
|
-
scope: "project",
|
|
27011
|
-
memory_tier: "reference",
|
|
27012
|
-
title: patternTitle,
|
|
27013
|
-
content: `Recurring pattern: ${entity.type} entities detected ${existing.length + 1} times.
|
|
27014
|
-
|
|
27015
|
-
Members:
|
|
27016
|
-
${memberTitles.map((t) => `- ${t}`).join(`
|
|
27017
|
-
`)}`,
|
|
27018
|
-
confidence: 0.75,
|
|
27019
|
-
tags: ["auto-extracted", "pattern", entity.type],
|
|
27020
|
-
metadata: {
|
|
27021
|
-
source: "pattern_detection",
|
|
27022
|
-
pattern_type: entity.type,
|
|
27023
|
-
pattern_count: existing.length + 1
|
|
27024
|
-
},
|
|
27025
|
-
agent_identifier: session.agentIdentifier
|
|
27026
|
-
});
|
|
27027
|
-
const created = result.entity;
|
|
27028
|
-
if (created?.id) {
|
|
27029
|
-
patternId = created.id;
|
|
27030
|
-
patternEntityIds.push(patternId);
|
|
27031
|
-
}
|
|
27032
|
-
}
|
|
27033
|
-
if (!patternId)
|
|
27034
|
-
continue;
|
|
27035
|
-
const toLink = [newEntityId, ...existing.slice(0, 4).map((e) => e.id)];
|
|
27036
|
-
for (const sourceId of toLink) {
|
|
27037
|
-
try {
|
|
27038
|
-
await client2.createMemoryRelation({
|
|
27039
|
-
source_id: sourceId,
|
|
27040
|
-
target_id: patternId,
|
|
27041
|
-
relation_type: "part_of",
|
|
27042
|
-
confidence: 0.75
|
|
27043
|
-
});
|
|
27044
|
-
} catch {}
|
|
27045
|
-
}
|
|
27046
|
-
} catch {}
|
|
27047
|
-
}
|
|
27048
|
-
return patternEntityIds;
|
|
27049
|
-
}
|
|
27050
|
-
var CAUSAL_PATTERN_THRESHOLD = 3;
|
|
27051
|
-
async function detectCausalPatterns(client2, createdPairs, session, workspaceId, projectId) {
|
|
27052
|
-
const patternIds = [];
|
|
27053
|
-
const errors3 = createdPairs.filter((p) => p.learning.type === "error");
|
|
27054
|
-
const solutions = createdPairs.filter((p) => p.learning.type === "solution");
|
|
27055
|
-
if (errors3.length === 0 || solutions.length === 0)
|
|
27056
|
-
return patternIds;
|
|
27057
|
-
for (const errorPair of errors3) {
|
|
27058
|
-
try {
|
|
27059
|
-
const similarErrors = await findSimilarEntities(client2, errorPair.learning.title, errorPair.learning.content, workspaceId, {
|
|
27060
|
-
projectId,
|
|
27061
|
-
limit: 20,
|
|
27062
|
-
minRrfScore: 0.03,
|
|
27063
|
-
excludeIds: createdPairs.map((p) => p.id),
|
|
27064
|
-
type: "error"
|
|
27065
|
-
});
|
|
27066
|
-
const resolvedErrors = [];
|
|
27067
|
-
for (const similar of similarErrors.slice(0, 10)) {
|
|
27068
|
-
try {
|
|
27069
|
-
const { outgoing } = await client2.getRelatedEntities(similar.id);
|
|
27070
|
-
const resolvedByRel = outgoing.find((r) => r.relation_type === "resolved_by");
|
|
27071
|
-
if (resolvedByRel) {
|
|
27072
|
-
resolvedErrors.push({
|
|
27073
|
-
errorId: similar.id,
|
|
27074
|
-
errorTitle: similar.title,
|
|
27075
|
-
solutionTitle: resolvedByRel.target_title || "unknown"
|
|
27076
|
-
});
|
|
27077
|
-
}
|
|
27078
|
-
} catch {}
|
|
27079
|
-
}
|
|
27080
|
-
if (resolvedErrors.length + 1 < CAUSAL_PATTERN_THRESHOLD)
|
|
27081
|
-
continue;
|
|
27082
|
-
const { entities: existingPatterns } = await client2.listMemoryEntities({
|
|
27083
|
-
workspace_id: workspaceId,
|
|
27084
|
-
project_id: projectId,
|
|
27085
|
-
type: "pattern",
|
|
27086
|
-
limit: 10
|
|
27087
|
-
});
|
|
27088
|
-
const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_chain_type === "error_resolved_by_solution");
|
|
27089
|
-
if (matchingPattern) {
|
|
27090
|
-
await client2.updateMemoryEntity(matchingPattern.id, {
|
|
27091
|
-
content: [
|
|
27092
|
-
`Recurring error→solution chain detected (${resolvedErrors.length + 1} instances).`,
|
|
27093
|
-
"",
|
|
27094
|
-
"## Error→Solution Pairs",
|
|
27095
|
-
`- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
|
|
27096
|
-
...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`),
|
|
27097
|
-
"",
|
|
27098
|
-
`Last updated: ${new Date().toISOString()}`
|
|
27099
|
-
].join(`
|
|
27100
|
-
`),
|
|
27101
|
-
metadata: {
|
|
27102
|
-
pattern_chain_type: "error_resolved_by_solution",
|
|
27103
|
-
pattern_count: resolvedErrors.length + 1,
|
|
27104
|
-
last_updated: new Date().toISOString()
|
|
27105
|
-
}
|
|
27106
|
-
});
|
|
27107
|
-
for (const pair of [errorPair, solutions[0]]) {
|
|
27108
|
-
try {
|
|
27109
|
-
await client2.createMemoryRelation({
|
|
27110
|
-
source_id: pair.id,
|
|
27111
|
-
target_id: matchingPattern.id,
|
|
27112
|
-
relation_type: "part_of",
|
|
27113
|
-
confidence: 0.75
|
|
27114
|
-
});
|
|
27115
|
-
} catch {}
|
|
27116
|
-
}
|
|
27117
|
-
} else {
|
|
27118
|
-
const result = await client2.createMemoryEntity({
|
|
27119
|
-
workspace_id: workspaceId,
|
|
27120
|
-
project_id: projectId,
|
|
27121
|
-
type: "pattern",
|
|
27122
|
-
scope: "project",
|
|
27123
|
-
memory_tier: "reference",
|
|
27124
|
-
title: `Pattern: recurring error→solution chain (${resolvedErrors.length + 1} instances)`,
|
|
27125
|
-
content: [
|
|
27126
|
-
`Recurring error→solution chain detected across ${resolvedErrors.length + 1} sessions.`,
|
|
27127
|
-
"",
|
|
27128
|
-
"## Error→Solution Pairs",
|
|
27129
|
-
`- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
|
|
27130
|
-
...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`)
|
|
27131
|
-
].join(`
|
|
27132
|
-
`),
|
|
27133
|
-
confidence: 0.8,
|
|
27134
|
-
tags: ["auto-extracted", "pattern", "causal-chain"],
|
|
27135
|
-
metadata: {
|
|
27136
|
-
source: "causal_pattern_detection",
|
|
27137
|
-
pattern_chain_type: "error_resolved_by_solution",
|
|
27138
|
-
pattern_count: resolvedErrors.length + 1
|
|
27139
|
-
},
|
|
27140
|
-
agent_identifier: session.agentIdentifier
|
|
27141
|
-
});
|
|
27142
|
-
const created = result.entity;
|
|
27143
|
-
if (created?.id) {
|
|
27144
|
-
patternIds.push(created.id);
|
|
27145
|
-
const memberIds = [
|
|
27146
|
-
errorPair.id,
|
|
27147
|
-
solutions[0].id,
|
|
27148
|
-
...resolvedErrors.slice(0, 4).map((r) => r.errorId)
|
|
27149
|
-
];
|
|
27150
|
-
for (const memberId of memberIds) {
|
|
27151
|
-
try {
|
|
27152
|
-
await client2.createMemoryRelation({
|
|
27153
|
-
source_id: memberId,
|
|
27154
|
-
target_id: created.id,
|
|
27155
|
-
relation_type: "part_of",
|
|
27156
|
-
confidence: 0.75
|
|
27157
|
-
});
|
|
27158
|
-
} catch {}
|
|
27159
|
-
}
|
|
27160
|
-
}
|
|
27161
|
-
}
|
|
27162
|
-
} catch {}
|
|
27163
|
-
}
|
|
27164
|
-
return patternIds;
|
|
27165
|
-
}
|
|
27166
26935
|
async function detectContradictions(client2, entityId, entityType, title, content, tags, workspaceId, projectId) {
|
|
27167
26936
|
if (!CONTRADICTION_TYPES.has(entityType))
|
|
27168
26937
|
return [];
|
|
@@ -27975,7 +27744,7 @@ async function autoEndSession(client3, cardId, status) {
|
|
|
27975
27744
|
// src/consolidation.ts
|
|
27976
27745
|
async function consolidateMemories(client3, workspaceId, projectId, options) {
|
|
27977
27746
|
const dryRun = options?.dryRun !== false;
|
|
27978
|
-
const minClusterSize = options?.minClusterSize ??
|
|
27747
|
+
const minClusterSize = options?.minClusterSize ?? 3;
|
|
27979
27748
|
const result = {
|
|
27980
27749
|
consolidated: 0,
|
|
27981
27750
|
clustersFound: 0,
|
|
@@ -28040,12 +27809,7 @@ async function consolidateMemories(client3, workspaceId, projectId, options) {
|
|
|
28040
27809
|
result.clustersFound++;
|
|
28041
27810
|
const mergedTitle = deriveClusterTitle(cluster, type);
|
|
28042
27811
|
const memberTitles = cluster.map((e) => e.title);
|
|
28043
|
-
const mergedContent =
|
|
28044
|
-
`Consolidated from ${cluster.length} ${type} memories:
|
|
28045
|
-
`,
|
|
28046
|
-
...cluster.map((e) => `- **${e.title}**: ${e.content.slice(0, 200)}`)
|
|
28047
|
-
].join(`
|
|
28048
|
-
`);
|
|
27812
|
+
const mergedContent = synthesizeClusterContent(cluster, type);
|
|
28049
27813
|
const maxConfidence = Math.max(...cluster.map((e) => e.confidence));
|
|
28050
27814
|
const allTags = [...new Set(cluster.flatMap((e) => e.tags || []))];
|
|
28051
27815
|
const detail = {
|
|
@@ -28107,6 +27871,60 @@ async function consolidateMemories(client3, workspaceId, projectId, options) {
|
|
|
28107
27871
|
}
|
|
28108
27872
|
return result;
|
|
28109
27873
|
}
|
|
27874
|
+
function synthesizeClusterContent(cluster, type) {
|
|
27875
|
+
const SKIP_PATTERNS = [
|
|
27876
|
+
/^##\s/,
|
|
27877
|
+
/^Agent:/,
|
|
27878
|
+
/^Duration:/,
|
|
27879
|
+
/^Labels:/,
|
|
27880
|
+
/^Progress:/,
|
|
27881
|
+
/^Session status:/,
|
|
27882
|
+
/^Completed at/,
|
|
27883
|
+
/^Final state:/,
|
|
27884
|
+
/^Related:/,
|
|
27885
|
+
/^When working on:/,
|
|
27886
|
+
/^\d+\.\s+.+\(\d+%,\s*\+\d+%\)/,
|
|
27887
|
+
/^Last updated:/,
|
|
27888
|
+
/^Recurring pattern:/,
|
|
27889
|
+
/^Consolidated from/
|
|
27890
|
+
];
|
|
27891
|
+
const seenLines = new Set;
|
|
27892
|
+
const knowledgeLines = [];
|
|
27893
|
+
for (const entity of cluster) {
|
|
27894
|
+
const lines = entity.content.split(`
|
|
27895
|
+
`).map((l) => l.trim());
|
|
27896
|
+
for (const line of lines) {
|
|
27897
|
+
if (!line || line.length < 20)
|
|
27898
|
+
continue;
|
|
27899
|
+
if (SKIP_PATTERNS.some((p) => p.test(line)))
|
|
27900
|
+
continue;
|
|
27901
|
+
const normalized = line.toLowerCase().replace(/[*_`#[\]]/g, "").trim();
|
|
27902
|
+
if (seenLines.has(normalized))
|
|
27903
|
+
continue;
|
|
27904
|
+
seenLines.add(normalized);
|
|
27905
|
+
knowledgeLines.push(line);
|
|
27906
|
+
}
|
|
27907
|
+
}
|
|
27908
|
+
if (knowledgeLines.length === 0) {
|
|
27909
|
+
return `${cluster.length} related ${type} entities consolidated. Original titles:
|
|
27910
|
+
${cluster.map((e) => `- ${e.title}`).join(`
|
|
27911
|
+
`)}`;
|
|
27912
|
+
}
|
|
27913
|
+
const MAX_CHARS = 1600;
|
|
27914
|
+
const result = [
|
|
27915
|
+
`Consolidated knowledge from ${cluster.length} ${type} entities:
|
|
27916
|
+
`
|
|
27917
|
+
];
|
|
27918
|
+
let charCount = result[0].length;
|
|
27919
|
+
for (const line of knowledgeLines) {
|
|
27920
|
+
if (charCount + line.length + 3 > MAX_CHARS)
|
|
27921
|
+
break;
|
|
27922
|
+
result.push(`- ${line}`);
|
|
27923
|
+
charCount += line.length + 3;
|
|
27924
|
+
}
|
|
27925
|
+
return result.join(`
|
|
27926
|
+
`);
|
|
27927
|
+
}
|
|
28110
27928
|
function deriveClusterTitle(cluster, type) {
|
|
28111
27929
|
const stopWords = new Set([
|
|
28112
27930
|
"the",
|
|
@@ -28161,9 +27979,9 @@ function deriveClusterTitle(cluster, type) {
|
|
|
28161
27979
|
wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
|
|
28162
27980
|
}
|
|
28163
27981
|
}
|
|
28164
|
-
const topWords = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0,
|
|
28165
|
-
const suffix = topWords.length > 0 ? topWords.join("
|
|
28166
|
-
return
|
|
27982
|
+
const topWords = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 4).map(([word]) => word[0].toUpperCase() + word.slice(1));
|
|
27983
|
+
const suffix = topWords.length > 0 ? topWords.join(" / ") : "Various";
|
|
27984
|
+
return `${type[0].toUpperCase() + type.slice(1)}: ${suffix}`;
|
|
28167
27985
|
}
|
|
28168
27986
|
|
|
28169
27987
|
// src/server.ts
|
|
@@ -28239,6 +28057,371 @@ async function runLifecycleMaintenance(client3, workspaceId, projectId) {
|
|
|
28239
28057
|
return result;
|
|
28240
28058
|
}
|
|
28241
28059
|
|
|
28060
|
+
// src/memory-cleanup.ts
|
|
28061
|
+
init_dist();
|
|
28062
|
+
var ALL_STEPS = [
|
|
28063
|
+
"prune",
|
|
28064
|
+
"consolidate",
|
|
28065
|
+
"orphans",
|
|
28066
|
+
"duplicates",
|
|
28067
|
+
"backfill"
|
|
28068
|
+
];
|
|
28069
|
+
async function runMemoryCleanup(client3, workspaceId, projectId, options) {
|
|
28070
|
+
const dryRun = options?.dryRun !== false;
|
|
28071
|
+
const steps = options?.steps ?? ALL_STEPS;
|
|
28072
|
+
const maxAgeDays = options?.maxAgeDays ?? 30;
|
|
28073
|
+
const minClusterSize = options?.minClusterSize ?? 3;
|
|
28074
|
+
const orphanAgeDays = options?.orphanAgeDays ?? 14;
|
|
28075
|
+
const report = {
|
|
28076
|
+
success: true,
|
|
28077
|
+
dryRun,
|
|
28078
|
+
timestamp: new Date().toISOString(),
|
|
28079
|
+
workspace: { id: workspaceId, projectId },
|
|
28080
|
+
summary: { totalEntities: 0, issuesFound: 0, actionsTaken: 0 },
|
|
28081
|
+
steps: {},
|
|
28082
|
+
errors: [],
|
|
28083
|
+
healthReport: ""
|
|
28084
|
+
};
|
|
28085
|
+
let entities = [];
|
|
28086
|
+
try {
|
|
28087
|
+
const listResult = await client3.listMemoryEntities({
|
|
28088
|
+
workspace_id: workspaceId,
|
|
28089
|
+
project_id: projectId,
|
|
28090
|
+
limit: 200
|
|
28091
|
+
});
|
|
28092
|
+
entities = listResult.entities || [];
|
|
28093
|
+
report.summary.totalEntities = entities.length;
|
|
28094
|
+
} catch (err) {
|
|
28095
|
+
report.errors.push({
|
|
28096
|
+
step: "init",
|
|
28097
|
+
message: `Failed to fetch entities: ${err.message}`
|
|
28098
|
+
});
|
|
28099
|
+
report.success = false;
|
|
28100
|
+
report.healthReport = generateHealthReport(report);
|
|
28101
|
+
return report;
|
|
28102
|
+
}
|
|
28103
|
+
if (steps.includes("prune")) {
|
|
28104
|
+
try {
|
|
28105
|
+
report.steps.prune = runPruneStep(entities, maxAgeDays);
|
|
28106
|
+
if (!dryRun) {
|
|
28107
|
+
for (const item of report.steps.prune.items) {
|
|
28108
|
+
try {
|
|
28109
|
+
await client3.deleteMemoryEntity(item.id);
|
|
28110
|
+
report.steps.prune.pruned++;
|
|
28111
|
+
} catch {}
|
|
28112
|
+
}
|
|
28113
|
+
report.summary.actionsTaken += report.steps.prune.pruned;
|
|
28114
|
+
}
|
|
28115
|
+
report.summary.issuesFound += report.steps.prune.staleDraftsFound;
|
|
28116
|
+
} catch (err) {
|
|
28117
|
+
report.errors.push({
|
|
28118
|
+
step: "prune",
|
|
28119
|
+
message: err.message
|
|
28120
|
+
});
|
|
28121
|
+
}
|
|
28122
|
+
}
|
|
28123
|
+
if (steps.includes("consolidate")) {
|
|
28124
|
+
try {
|
|
28125
|
+
const result = await consolidateMemories(client3, workspaceId, projectId, {
|
|
28126
|
+
dryRun,
|
|
28127
|
+
minClusterSize
|
|
28128
|
+
});
|
|
28129
|
+
report.steps.consolidate = {
|
|
28130
|
+
clustersFound: result.clustersFound,
|
|
28131
|
+
entitiesProcessed: result.entitiesProcessed,
|
|
28132
|
+
consolidated: result.consolidated,
|
|
28133
|
+
details: result.details
|
|
28134
|
+
};
|
|
28135
|
+
report.summary.issuesFound += result.clustersFound;
|
|
28136
|
+
if (!dryRun)
|
|
28137
|
+
report.summary.actionsTaken += result.consolidated;
|
|
28138
|
+
} catch (err) {
|
|
28139
|
+
report.errors.push({
|
|
28140
|
+
step: "consolidate",
|
|
28141
|
+
message: err.message
|
|
28142
|
+
});
|
|
28143
|
+
}
|
|
28144
|
+
}
|
|
28145
|
+
if (steps.includes("orphans")) {
|
|
28146
|
+
try {
|
|
28147
|
+
report.steps.orphans = await runOrphanStep(client3, entities, orphanAgeDays);
|
|
28148
|
+
if (!dryRun) {
|
|
28149
|
+
for (const item of report.steps.orphans.items) {
|
|
28150
|
+
try {
|
|
28151
|
+
await client3.deleteMemoryEntity(item.id);
|
|
28152
|
+
report.steps.orphans.removed++;
|
|
28153
|
+
} catch {}
|
|
28154
|
+
}
|
|
28155
|
+
report.summary.actionsTaken += report.steps.orphans.removed;
|
|
28156
|
+
}
|
|
28157
|
+
report.summary.issuesFound += report.steps.orphans.orphansFound;
|
|
28158
|
+
} catch (err) {
|
|
28159
|
+
report.errors.push({
|
|
28160
|
+
step: "orphans",
|
|
28161
|
+
message: err.message
|
|
28162
|
+
});
|
|
28163
|
+
}
|
|
28164
|
+
}
|
|
28165
|
+
if (steps.includes("duplicates")) {
|
|
28166
|
+
try {
|
|
28167
|
+
report.steps.duplicates = await runDuplicateStep(client3, entities, workspaceId, projectId);
|
|
28168
|
+
if (!dryRun) {
|
|
28169
|
+
for (const pair of report.steps.duplicates.pairs) {
|
|
28170
|
+
try {
|
|
28171
|
+
await client3.deleteMemoryEntity(pair.removeId);
|
|
28172
|
+
report.steps.duplicates.resolved++;
|
|
28173
|
+
} catch {}
|
|
28174
|
+
}
|
|
28175
|
+
report.summary.actionsTaken += report.steps.duplicates.resolved;
|
|
28176
|
+
}
|
|
28177
|
+
report.summary.issuesFound += report.steps.duplicates.duplicatePairsFound;
|
|
28178
|
+
} catch (err) {
|
|
28179
|
+
report.errors.push({
|
|
28180
|
+
step: "duplicates",
|
|
28181
|
+
message: err.message
|
|
28182
|
+
});
|
|
28183
|
+
}
|
|
28184
|
+
}
|
|
28185
|
+
if (steps.includes("backfill")) {
|
|
28186
|
+
try {
|
|
28187
|
+
if (dryRun) {
|
|
28188
|
+
report.steps.backfill = {
|
|
28189
|
+
processed: 0,
|
|
28190
|
+
remaining: -1,
|
|
28191
|
+
errors: []
|
|
28192
|
+
};
|
|
28193
|
+
} else {
|
|
28194
|
+
const result = await client3.backfillEmbeddings(workspaceId);
|
|
28195
|
+
report.steps.backfill = {
|
|
28196
|
+
processed: result.processed,
|
|
28197
|
+
remaining: result.remaining,
|
|
28198
|
+
errors: result.errors || []
|
|
28199
|
+
};
|
|
28200
|
+
report.summary.actionsTaken += result.processed;
|
|
28201
|
+
}
|
|
28202
|
+
} catch (err) {
|
|
28203
|
+
report.errors.push({
|
|
28204
|
+
step: "backfill",
|
|
28205
|
+
message: err.message
|
|
28206
|
+
});
|
|
28207
|
+
}
|
|
28208
|
+
}
|
|
28209
|
+
report.healthReport = generateHealthReport(report);
|
|
28210
|
+
return report;
|
|
28211
|
+
}
|
|
28212
|
+
function runPruneStep(entities, maxAgeDays) {
|
|
28213
|
+
const now = Date.now();
|
|
28214
|
+
const drafts = entities.filter((e) => e.memory_tier === "draft");
|
|
28215
|
+
const stale = [];
|
|
28216
|
+
for (const entity of drafts) {
|
|
28217
|
+
const ageDays = (now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
28218
|
+
if (ageDays < maxAgeDays)
|
|
28219
|
+
continue;
|
|
28220
|
+
const lifecycle2 = evaluateLifecycle(entity);
|
|
28221
|
+
stale.push({
|
|
28222
|
+
id: entity.id,
|
|
28223
|
+
title: entity.title,
|
|
28224
|
+
ageDays: Math.round(ageDays),
|
|
28225
|
+
decayScore: Math.round(lifecycle2.decay.score * 100) / 100
|
|
28226
|
+
});
|
|
28227
|
+
}
|
|
28228
|
+
return { staleDraftsFound: stale.length, pruned: 0, items: stale };
|
|
28229
|
+
}
|
|
28230
|
+
async function runOrphanStep(client3, entities, orphanAgeDays) {
|
|
28231
|
+
const now = Date.now();
|
|
28232
|
+
const result = { orphansFound: 0, removed: 0, items: [] };
|
|
28233
|
+
const candidates = entities.filter((e) => {
|
|
28234
|
+
if (e.memory_tier === "reference")
|
|
28235
|
+
return false;
|
|
28236
|
+
if (e.access_count >= 2)
|
|
28237
|
+
return false;
|
|
28238
|
+
const ageDays = (now - new Date(e.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
28239
|
+
return ageDays >= orphanAgeDays;
|
|
28240
|
+
});
|
|
28241
|
+
for (const entity of candidates) {
|
|
28242
|
+
try {
|
|
28243
|
+
const related = await client3.getRelatedEntities(entity.id);
|
|
28244
|
+
const totalRelations = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
|
|
28245
|
+
if (totalRelations > 0)
|
|
28246
|
+
continue;
|
|
28247
|
+
const ageDays = (now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
28248
|
+
result.items.push({
|
|
28249
|
+
id: entity.id,
|
|
28250
|
+
title: entity.title,
|
|
28251
|
+
type: entity.type,
|
|
28252
|
+
tier: entity.memory_tier,
|
|
28253
|
+
ageDays: Math.round(ageDays),
|
|
28254
|
+
accessCount: entity.access_count
|
|
28255
|
+
});
|
|
28256
|
+
result.orphansFound++;
|
|
28257
|
+
} catch {}
|
|
28258
|
+
}
|
|
28259
|
+
return result;
|
|
28260
|
+
}
|
|
28261
|
+
async function runDuplicateStep(client3, entities, workspaceId, projectId) {
|
|
28262
|
+
const result = {
|
|
28263
|
+
duplicatePairsFound: 0,
|
|
28264
|
+
resolved: 0,
|
|
28265
|
+
pairs: []
|
|
28266
|
+
};
|
|
28267
|
+
const seenPairs = new Set;
|
|
28268
|
+
const flaggedForRemoval = new Set;
|
|
28269
|
+
for (const entity of entities) {
|
|
28270
|
+
if (flaggedForRemoval.has(entity.id))
|
|
28271
|
+
continue;
|
|
28272
|
+
let similar;
|
|
28273
|
+
try {
|
|
28274
|
+
similar = await findSimilarEntities(client3, entity.title, entity.content, workspaceId, { projectId, limit: 5, minRrfScore: 0.05, excludeIds: [entity.id] });
|
|
28275
|
+
} catch {
|
|
28276
|
+
continue;
|
|
28277
|
+
}
|
|
28278
|
+
for (const match of similar) {
|
|
28279
|
+
if (flaggedForRemoval.has(match.id))
|
|
28280
|
+
continue;
|
|
28281
|
+
const pairKey = [entity.id, match.id].sort().join(":");
|
|
28282
|
+
if (seenPairs.has(pairKey))
|
|
28283
|
+
continue;
|
|
28284
|
+
seenPairs.add(pairKey);
|
|
28285
|
+
const sim = titleSimilarity(entity.title, match.title);
|
|
28286
|
+
if (sim < 0.85)
|
|
28287
|
+
continue;
|
|
28288
|
+
const entityScore = entityQualityScore(entity);
|
|
28289
|
+
const matchEntity = entities.find((e) => e.id === match.id);
|
|
28290
|
+
const matchScore = matchEntity ? entityQualityScore(matchEntity) : match.confidence;
|
|
28291
|
+
const [keep, remove] = entityScore >= matchScore ? [entity, { id: match.id, title: match.title }] : [{ id: match.id, title: match.title }, entity];
|
|
28292
|
+
flaggedForRemoval.add(remove.id);
|
|
28293
|
+
result.pairs.push({
|
|
28294
|
+
keepId: keep.id,
|
|
28295
|
+
keepTitle: keep.title,
|
|
28296
|
+
removeId: remove.id,
|
|
28297
|
+
removeTitle: remove.title,
|
|
28298
|
+
similarity: Math.round(sim * 100) / 100
|
|
28299
|
+
});
|
|
28300
|
+
result.duplicatePairsFound++;
|
|
28301
|
+
}
|
|
28302
|
+
}
|
|
28303
|
+
return result;
|
|
28304
|
+
}
|
|
28305
|
+
var TIER_WEIGHTS2 = {
|
|
28306
|
+
reference: 3,
|
|
28307
|
+
episode: 2,
|
|
28308
|
+
draft: 1
|
|
28309
|
+
};
|
|
28310
|
+
function entityQualityScore(entity) {
|
|
28311
|
+
return entity.confidence + (TIER_WEIGHTS2[entity.memory_tier] || 0) + Math.min(entity.access_count, 10) * 0.1;
|
|
28312
|
+
}
|
|
28313
|
+
function titleSimilarity(a, b) {
|
|
28314
|
+
const na = a.toLowerCase().trim();
|
|
28315
|
+
const nb = b.toLowerCase().trim();
|
|
28316
|
+
if (na === nb)
|
|
28317
|
+
return 1;
|
|
28318
|
+
const wordsA = new Set(na.split(/\W+/).filter(Boolean));
|
|
28319
|
+
const wordsB = new Set(nb.split(/\W+/).filter(Boolean));
|
|
28320
|
+
if (wordsA.size === 0 || wordsB.size === 0)
|
|
28321
|
+
return 0;
|
|
28322
|
+
let intersection3 = 0;
|
|
28323
|
+
for (const w of wordsA) {
|
|
28324
|
+
if (wordsB.has(w))
|
|
28325
|
+
intersection3++;
|
|
28326
|
+
}
|
|
28327
|
+
const union3 = wordsA.size + wordsB.size - intersection3;
|
|
28328
|
+
return union3 > 0 ? intersection3 / union3 : 0;
|
|
28329
|
+
}
|
|
28330
|
+
function generateHealthReport(report) {
|
|
28331
|
+
const mode = report.dryRun ? "Dry Run (preview)" : "Executed";
|
|
28332
|
+
const lines = [
|
|
28333
|
+
`# Memory Health Report
|
|
28334
|
+
`,
|
|
28335
|
+
`**Mode:** ${mode} | **Entities:** ${report.summary.totalEntities} | **Issues:** ${report.summary.issuesFound} | **Actions:** ${report.summary.actionsTaken}`,
|
|
28336
|
+
""
|
|
28337
|
+
];
|
|
28338
|
+
if (report.steps.prune) {
|
|
28339
|
+
const p = report.steps.prune;
|
|
28340
|
+
lines.push("## Stale Drafts");
|
|
28341
|
+
if (p.staleDraftsFound === 0) {
|
|
28342
|
+
lines.push(`No stale drafts found.
|
|
28343
|
+
`);
|
|
28344
|
+
} else {
|
|
28345
|
+
lines.push(`Found **${p.staleDraftsFound}** stale drafts${!report.dryRun ? ` (pruned ${p.pruned})` : ""}:`);
|
|
28346
|
+
lines.push("| Title | Age | Decay |");
|
|
28347
|
+
lines.push("|-------|-----|-------|");
|
|
28348
|
+
for (const item of p.items.slice(0, 20)) {
|
|
28349
|
+
lines.push(`| ${item.title} | ${item.ageDays}d | ${item.decayScore} |`);
|
|
28350
|
+
}
|
|
28351
|
+
lines.push("");
|
|
28352
|
+
}
|
|
28353
|
+
}
|
|
28354
|
+
if (report.steps.consolidate) {
|
|
28355
|
+
const c = report.steps.consolidate;
|
|
28356
|
+
lines.push("## Consolidation");
|
|
28357
|
+
if (c.clustersFound === 0) {
|
|
28358
|
+
lines.push(`Scanned ${c.entitiesProcessed} draft/episode entities — no clusters found.
|
|
28359
|
+
`);
|
|
28360
|
+
} else {
|
|
28361
|
+
lines.push(`Found **${c.clustersFound}** clusters across ${c.entitiesProcessed} entities:`);
|
|
28362
|
+
for (const d of c.details.slice(0, 10)) {
|
|
28363
|
+
lines.push(`- **${d.mergedTitle}** — ${d.clusterSize} entities`);
|
|
28364
|
+
}
|
|
28365
|
+
lines.push("");
|
|
28366
|
+
}
|
|
28367
|
+
}
|
|
28368
|
+
if (report.steps.orphans) {
|
|
28369
|
+
const o = report.steps.orphans;
|
|
28370
|
+
lines.push("## Orphaned Entities");
|
|
28371
|
+
if (o.orphansFound === 0) {
|
|
28372
|
+
lines.push(`No orphans found.
|
|
28373
|
+
`);
|
|
28374
|
+
} else {
|
|
28375
|
+
lines.push(`Found **${o.orphansFound}** orphans${!report.dryRun ? ` (removed ${o.removed})` : ""}:`);
|
|
28376
|
+
lines.push("| Title | Type | Tier | Age | Accesses |");
|
|
28377
|
+
lines.push("|-------|------|------|-----|----------|");
|
|
28378
|
+
for (const item of o.items.slice(0, 20)) {
|
|
28379
|
+
lines.push(`| ${item.title} | ${item.type} | ${item.tier} | ${item.ageDays}d | ${item.accessCount} |`);
|
|
28380
|
+
}
|
|
28381
|
+
lines.push("");
|
|
28382
|
+
}
|
|
28383
|
+
}
|
|
28384
|
+
if (report.steps.duplicates) {
|
|
28385
|
+
const d = report.steps.duplicates;
|
|
28386
|
+
lines.push("## Near-Duplicates");
|
|
28387
|
+
if (d.duplicatePairsFound === 0) {
|
|
28388
|
+
lines.push(`No duplicates found.
|
|
28389
|
+
`);
|
|
28390
|
+
} else {
|
|
28391
|
+
lines.push(`Found **${d.duplicatePairsFound}** duplicate pairs${!report.dryRun ? ` (resolved ${d.resolved})` : ""}:`);
|
|
28392
|
+
for (const pair of d.pairs.slice(0, 20)) {
|
|
28393
|
+
lines.push(`- "${pair.keepTitle}" ~ "${pair.removeTitle}" (${Math.round(pair.similarity * 100)}% similar, keep first)`);
|
|
28394
|
+
}
|
|
28395
|
+
lines.push("");
|
|
28396
|
+
}
|
|
28397
|
+
}
|
|
28398
|
+
if (report.steps.backfill) {
|
|
28399
|
+
const b = report.steps.backfill;
|
|
28400
|
+
lines.push("## Embedding Coverage");
|
|
28401
|
+
if (report.dryRun) {
|
|
28402
|
+
lines.push("Backfill will run when executed with `dryRun: false`.\n");
|
|
28403
|
+
} else if (b.remaining === 0) {
|
|
28404
|
+
lines.push(`All embeddings up to date (processed ${b.processed}).
|
|
28405
|
+
`);
|
|
28406
|
+
} else {
|
|
28407
|
+
lines.push(`Processed ${b.processed} entities. ${b.remaining} still need embeddings.
|
|
28408
|
+
`);
|
|
28409
|
+
}
|
|
28410
|
+
}
|
|
28411
|
+
if (report.errors.length > 0) {
|
|
28412
|
+
lines.push("## Errors");
|
|
28413
|
+
for (const e of report.errors) {
|
|
28414
|
+
lines.push(`- **${e.step}:** ${e.message}`);
|
|
28415
|
+
}
|
|
28416
|
+
lines.push("");
|
|
28417
|
+
}
|
|
28418
|
+
if (report.dryRun) {
|
|
28419
|
+
lines.push("---\n*Run with `dryRun: false` to execute cleanup.*");
|
|
28420
|
+
}
|
|
28421
|
+
return lines.join(`
|
|
28422
|
+
`);
|
|
28423
|
+
}
|
|
28424
|
+
|
|
28242
28425
|
// src/onboard.ts
|
|
28243
28426
|
async function onboardNewUser(params) {
|
|
28244
28427
|
const {
|
|
@@ -29673,6 +29856,47 @@ var TOOLS = {
|
|
|
29673
29856
|
},
|
|
29674
29857
|
required: []
|
|
29675
29858
|
}
|
|
29859
|
+
},
|
|
29860
|
+
harmony_cleanup_memories: {
|
|
29861
|
+
description: "Run a unified memory cleanup: prune stale drafts, consolidate similar memories, detect orphans and duplicates, and backfill embeddings. Returns a health report. Dry-run by default — run with dryRun=false to execute.",
|
|
29862
|
+
inputSchema: {
|
|
29863
|
+
type: "object",
|
|
29864
|
+
properties: {
|
|
29865
|
+
workspaceId: {
|
|
29866
|
+
type: "string",
|
|
29867
|
+
description: "Workspace ID (optional if context set)"
|
|
29868
|
+
},
|
|
29869
|
+
projectId: {
|
|
29870
|
+
type: "string",
|
|
29871
|
+
description: "Project ID (optional)"
|
|
29872
|
+
},
|
|
29873
|
+
dryRun: {
|
|
29874
|
+
type: "boolean",
|
|
29875
|
+
description: "Preview cleanup without executing changes (default: true)"
|
|
29876
|
+
},
|
|
29877
|
+
steps: {
|
|
29878
|
+
type: "array",
|
|
29879
|
+
items: {
|
|
29880
|
+
type: "string",
|
|
29881
|
+
enum: ["prune", "consolidate", "orphans", "duplicates", "backfill"]
|
|
29882
|
+
},
|
|
29883
|
+
description: "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill."
|
|
29884
|
+
},
|
|
29885
|
+
maxAgeDays: {
|
|
29886
|
+
type: "number",
|
|
29887
|
+
description: "Max age in days for stale draft pruning (default: 30)"
|
|
29888
|
+
},
|
|
29889
|
+
minClusterSize: {
|
|
29890
|
+
type: "number",
|
|
29891
|
+
description: "Min cluster size for consolidation (default: 3)"
|
|
29892
|
+
},
|
|
29893
|
+
orphanAgeDays: {
|
|
29894
|
+
type: "number",
|
|
29895
|
+
description: "Min age in days for orphan detection (default: 14)"
|
|
29896
|
+
}
|
|
29897
|
+
},
|
|
29898
|
+
required: []
|
|
29899
|
+
}
|
|
29676
29900
|
}
|
|
29677
29901
|
};
|
|
29678
29902
|
var RESOURCES = [
|
|
@@ -31183,6 +31407,27 @@ async function handleToolCall(name, args, deps) {
|
|
|
31183
31407
|
message: `Processed ${entitiesProcessed} entities, created ${relationsCreated} new relations.`
|
|
31184
31408
|
};
|
|
31185
31409
|
}
|
|
31410
|
+
case "harmony_cleanup_memories": {
|
|
31411
|
+
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
|
31412
|
+
if (!workspaceId) {
|
|
31413
|
+
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
31414
|
+
}
|
|
31415
|
+
const projectId = args.projectId || deps.getActiveProjectId() || undefined;
|
|
31416
|
+
const report = await runMemoryCleanup(client3, workspaceId, projectId, {
|
|
31417
|
+
dryRun: args.dryRun,
|
|
31418
|
+
steps: args.steps,
|
|
31419
|
+
maxAgeDays: args.maxAgeDays,
|
|
31420
|
+
minClusterSize: args.minClusterSize,
|
|
31421
|
+
orphanAgeDays: args.orphanAgeDays
|
|
31422
|
+
});
|
|
31423
|
+
return {
|
|
31424
|
+
success: report.success,
|
|
31425
|
+
dryRun: report.dryRun,
|
|
31426
|
+
summary: report.summary,
|
|
31427
|
+
errors: report.errors,
|
|
31428
|
+
healthReport: report.healthReport
|
|
31429
|
+
};
|
|
31430
|
+
}
|
|
31186
31431
|
default:
|
|
31187
31432
|
throw new Error(`Unknown tool: ${name}`);
|
|
31188
31433
|
}
|