@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/index.js
CHANGED
|
@@ -7078,6 +7078,29 @@ __export(exports_context_assembly, {
|
|
|
7078
7078
|
function estimateTokens(text) {
|
|
7079
7079
|
return Math.ceil(text.length / 4);
|
|
7080
7080
|
}
|
|
7081
|
+
function passesQualityGate(entity) {
|
|
7082
|
+
const content = entity.content.trim();
|
|
7083
|
+
if (content.length < 50)
|
|
7084
|
+
return false;
|
|
7085
|
+
const normalizedTitle = entity.title.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim();
|
|
7086
|
+
const normalizedContent = content.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim();
|
|
7087
|
+
if (normalizedContent.length < normalizedTitle.length * 1.5) {
|
|
7088
|
+
return false;
|
|
7089
|
+
}
|
|
7090
|
+
if (entity.type === "pattern" && /recurring .+ \(\d+ instances\)/i.test(entity.title)) {
|
|
7091
|
+
const lines = content.split(`
|
|
7092
|
+
`).filter((l) => l.trim().length > 0);
|
|
7093
|
+
const bulletLines = lines.filter((l) => l.trim().startsWith("- "));
|
|
7094
|
+
if (bulletLines.length > lines.length * 0.6)
|
|
7095
|
+
return false;
|
|
7096
|
+
}
|
|
7097
|
+
if (entity.type === "procedure") {
|
|
7098
|
+
const stepCount = (content.match(/^\d+\.\s/gm) || []).length;
|
|
7099
|
+
if (stepCount < 3)
|
|
7100
|
+
return false;
|
|
7101
|
+
}
|
|
7102
|
+
return true;
|
|
7103
|
+
}
|
|
7081
7104
|
function generateAssemblyId() {
|
|
7082
7105
|
return `ctx_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
7083
7106
|
}
|
|
@@ -7337,7 +7360,27 @@ async function assembleContext(options) {
|
|
|
7337
7360
|
memories: []
|
|
7338
7361
|
};
|
|
7339
7362
|
}
|
|
7340
|
-
const
|
|
7363
|
+
const qualityCandidates = candidates.filter((entity) => {
|
|
7364
|
+
if (passesQualityGate(entity))
|
|
7365
|
+
return true;
|
|
7366
|
+
manifest.excluded.push({
|
|
7367
|
+
entityId: entity.id,
|
|
7368
|
+
title: entity.title,
|
|
7369
|
+
type: entity.type,
|
|
7370
|
+
tier: entity.memory_tier,
|
|
7371
|
+
relevanceScore: 0,
|
|
7372
|
+
reason: "failed_quality_gate"
|
|
7373
|
+
});
|
|
7374
|
+
return false;
|
|
7375
|
+
});
|
|
7376
|
+
if (qualityCandidates.length === 0) {
|
|
7377
|
+
return {
|
|
7378
|
+
context: "",
|
|
7379
|
+
manifest,
|
|
7380
|
+
memories: []
|
|
7381
|
+
};
|
|
7382
|
+
}
|
|
7383
|
+
const scored = qualityCandidates.map((entity) => {
|
|
7341
7384
|
const { score, reasons } = computeRelevanceScore(entity, taskContext, cardLabels, graphRelations.length > 0 ? graphRelations : undefined);
|
|
7342
7385
|
return { entity, score, reasons };
|
|
7343
7386
|
});
|
|
@@ -7674,7 +7717,7 @@ async function recordContextFeedback(client2, cardId, sessionStatus, progressPer
|
|
|
7674
7717
|
sessionAssemblyMap.delete(cardId);
|
|
7675
7718
|
return { adjusted };
|
|
7676
7719
|
}
|
|
7677
|
-
var DEFAULT_TOKEN_BUDGET = 4000, MAX_TOKENS_PER_ENTITY = 500, MIN_RELEVANCE_THRESHOLD = 0.
|
|
7720
|
+
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;
|
|
7678
7721
|
var init_context_assembly = __esm(() => {
|
|
7679
7722
|
init_dist();
|
|
7680
7723
|
TIER_WEIGHTS = {
|
|
@@ -7842,7 +7885,11 @@ ${card.description}`);
|
|
|
7842
7885
|
roleFraming.focus.forEach((f) => {
|
|
7843
7886
|
sections.push(`- ${f}`);
|
|
7844
7887
|
});
|
|
7845
|
-
sections.push(`- **Memory:**
|
|
7888
|
+
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).`);
|
|
7889
|
+
sections.push(` - GOOD: "BoardContext card state must use moveCard action, never direct setState — optimistic updates depend on action ordering"`);
|
|
7890
|
+
sections.push(` - GOOD: "Mobile bottom bar is 64px, overlaps fixed-position drawers — always add pb-16 to drawer content"`);
|
|
7891
|
+
sections.push(` - BAD: "Fixed the login button" (no reusable knowledge — the fix is in the code)`);
|
|
7892
|
+
sections.push(` - BAD: "Completed card #42" (ephemeral, auto-tracked by session)`);
|
|
7846
7893
|
sections.push(`
|
|
7847
7894
|
## Suggested Outputs`);
|
|
7848
7895
|
roleFraming.outputSuggestions.forEach((s) => {
|
|
@@ -24291,13 +24338,13 @@ function levenshteinSimilarity(a, b) {
|
|
|
24291
24338
|
const maxLen = Math.max(sa.length, sb.length);
|
|
24292
24339
|
return 1 - matrix[sa.length][sb.length] / maxLen;
|
|
24293
24340
|
}
|
|
24294
|
-
async function extractMidSessionLearnings(
|
|
24341
|
+
async function extractMidSessionLearnings(_client, ctx) {
|
|
24295
24342
|
const workspaceId = getActiveWorkspaceId();
|
|
24296
24343
|
if (!workspaceId)
|
|
24297
24344
|
return { count: 0, entityIds: [] };
|
|
24298
|
-
const
|
|
24345
|
+
const _projectId = getActiveProjectId() || undefined;
|
|
24299
24346
|
const now = Date.now();
|
|
24300
|
-
const
|
|
24347
|
+
const _entityIds = [];
|
|
24301
24348
|
const history = sessionTaskHistory.get(ctx.cardId);
|
|
24302
24349
|
if (ctx.currentTask) {
|
|
24303
24350
|
const previousTask = history?.lastTask || "";
|
|
@@ -24330,81 +24377,22 @@ async function extractMidSessionLearnings(client2, ctx) {
|
|
|
24330
24377
|
}
|
|
24331
24378
|
}
|
|
24332
24379
|
if (ctx.status === "blocked" && ctx.blockers?.length) {
|
|
24333
|
-
for (const blocker of ctx.blockers) {
|
|
24334
|
-
try {
|
|
24335
|
-
const result = await client2.createMemoryEntity({
|
|
24336
|
-
workspace_id: workspaceId,
|
|
24337
|
-
project_id: projectId,
|
|
24338
|
-
type: "error",
|
|
24339
|
-
scope: "project",
|
|
24340
|
-
memory_tier: "draft",
|
|
24341
|
-
title: `Blocker (mid-session): ${blocker.slice(0, 100)}`,
|
|
24342
|
-
content: `Encountered while working on "${ctx.cardTitle}":
|
|
24343
|
-
|
|
24344
|
-
${blocker}
|
|
24345
|
-
|
|
24346
|
-
Agent: ${ctx.agentName}
|
|
24347
|
-
Progress: ${ctx.progressPercent ?? "unknown"}%`,
|
|
24348
|
-
confidence: 0.5,
|
|
24349
|
-
tags: ["auto-extracted", "blocker", "mid-session"],
|
|
24350
|
-
metadata: {
|
|
24351
|
-
source: "mid_session",
|
|
24352
|
-
card_id: ctx.cardId
|
|
24353
|
-
},
|
|
24354
|
-
agent_identifier: ctx.agentIdentifier
|
|
24355
|
-
});
|
|
24356
|
-
const entity = result.entity;
|
|
24357
|
-
if (entity?.id)
|
|
24358
|
-
entityIds.push(entity.id);
|
|
24359
|
-
} catch {}
|
|
24360
|
-
}
|
|
24361
24380
|
sessionTaskHistory.set(ctx.cardId, {
|
|
24362
24381
|
lastTask: ctx.currentTask || "",
|
|
24363
24382
|
lastExtractionAt: now,
|
|
24364
24383
|
steps: history?.steps || []
|
|
24365
24384
|
});
|
|
24366
|
-
return { count:
|
|
24385
|
+
return { count: 0, entityIds: [] };
|
|
24367
24386
|
}
|
|
24368
24387
|
if (ctx.currentTask) {
|
|
24369
|
-
const previousTask = history?.lastTask || "";
|
|
24370
|
-
const similarity = levenshteinSimilarity(previousTask, ctx.currentTask);
|
|
24371
|
-
if (similarity < 0.6 && previousTask.length > 0) {
|
|
24372
|
-
try {
|
|
24373
|
-
const result = await client2.createMemoryEntity({
|
|
24374
|
-
workspace_id: workspaceId,
|
|
24375
|
-
project_id: projectId,
|
|
24376
|
-
type: "context",
|
|
24377
|
-
scope: "project",
|
|
24378
|
-
memory_tier: "draft",
|
|
24379
|
-
title: `Task transition: ${ctx.cardTitle}`,
|
|
24380
|
-
content: `Agent transitioned tasks on "${ctx.cardTitle}".
|
|
24381
|
-
|
|
24382
|
-
Previous: ${previousTask}
|
|
24383
|
-
Current: ${ctx.currentTask}
|
|
24384
|
-
Progress: ${ctx.progressPercent ?? "unknown"}%`,
|
|
24385
|
-
confidence: 0.5,
|
|
24386
|
-
tags: ["auto-extracted", "task-transition", "mid-session"],
|
|
24387
|
-
metadata: {
|
|
24388
|
-
source: "mid_session",
|
|
24389
|
-
card_id: ctx.cardId,
|
|
24390
|
-
previous_task: previousTask,
|
|
24391
|
-
current_task: ctx.currentTask
|
|
24392
|
-
},
|
|
24393
|
-
agent_identifier: ctx.agentIdentifier
|
|
24394
|
-
});
|
|
24395
|
-
const entity = result.entity;
|
|
24396
|
-
if (entity?.id)
|
|
24397
|
-
entityIds.push(entity.id);
|
|
24398
|
-
} catch {}
|
|
24399
|
-
}
|
|
24400
24388
|
const currentHistory = sessionTaskHistory.get(ctx.cardId);
|
|
24401
24389
|
sessionTaskHistory.set(ctx.cardId, {
|
|
24402
24390
|
lastTask: ctx.currentTask,
|
|
24403
|
-
lastExtractionAt:
|
|
24391
|
+
lastExtractionAt: currentHistory?.lastExtractionAt ?? 0,
|
|
24404
24392
|
steps: currentHistory?.steps || []
|
|
24405
24393
|
});
|
|
24406
24394
|
}
|
|
24407
|
-
return { count:
|
|
24395
|
+
return { count: 0, entityIds: [] };
|
|
24408
24396
|
}
|
|
24409
24397
|
function clearMidSessionTracking(cardId) {
|
|
24410
24398
|
sessionTaskHistory.delete(cardId);
|
|
@@ -24588,49 +24576,60 @@ async function extractLearnings(client2, session) {
|
|
|
24588
24576
|
Related: ${relatedEntityTitles.map((t) => `[[${t}]]`).join(", ")}` : "";
|
|
24589
24577
|
if (session.blockers && session.blockers.length > 0) {
|
|
24590
24578
|
for (const blocker of session.blockers) {
|
|
24591
|
-
|
|
24592
|
-
|
|
24593
|
-
|
|
24579
|
+
if (blocker.length < 80)
|
|
24580
|
+
continue;
|
|
24581
|
+
let isDuplicate = false;
|
|
24582
|
+
try {
|
|
24583
|
+
const similar = await findSimilarEntities(client2, blocker.slice(0, 200), blocker, workspaceId, { projectId, limit: 3, minRrfScore: 0.05 });
|
|
24584
|
+
isDuplicate = similar.some((e) => e.type === "error" && (e.rrf_score ?? 0) >= 0.06);
|
|
24585
|
+
} catch {}
|
|
24586
|
+
if (!isDuplicate) {
|
|
24587
|
+
learnings.push({
|
|
24588
|
+
title: `Blocker: ${blocker.slice(0, 100)}`,
|
|
24589
|
+
content: `Encountered while working on "${session.cardTitle}":
|
|
24594
24590
|
|
|
24595
24591
|
${blocker}
|
|
24596
24592
|
|
|
24597
24593
|
Agent: ${session.agentName}
|
|
24598
24594
|
Session status: ${session.status}`,
|
|
24599
|
-
|
|
24600
|
-
|
|
24601
|
-
|
|
24602
|
-
|
|
24603
|
-
|
|
24604
|
-
|
|
24605
|
-
|
|
24606
|
-
|
|
24607
|
-
|
|
24595
|
+
type: "error",
|
|
24596
|
+
tier: "episode",
|
|
24597
|
+
confidence: 0.6,
|
|
24598
|
+
tags: [
|
|
24599
|
+
"auto-extracted",
|
|
24600
|
+
"blocker",
|
|
24601
|
+
...session.cardLabels.slice(0, 3)
|
|
24602
|
+
],
|
|
24603
|
+
metadata: {
|
|
24604
|
+
source: "active_learning",
|
|
24605
|
+
card_id: session.cardId
|
|
24606
|
+
}
|
|
24607
|
+
});
|
|
24608
|
+
}
|
|
24608
24609
|
}
|
|
24609
24610
|
}
|
|
24610
|
-
|
|
24611
|
-
if (session.status === "completed" && hasMeaningfulContent) {
|
|
24611
|
+
if (session.status === "paused" && (session.blockers?.length ?? 0) > 0) {
|
|
24612
24612
|
const durationInfo = session.sessionDurationMs ? `
|
|
24613
24613
|
Duration: ${Math.round(session.sessionDurationMs / 60000)} minutes` : "";
|
|
24614
24614
|
learnings.push({
|
|
24615
|
-
title: `
|
|
24615
|
+
title: `Paused: ${session.cardTitle}`,
|
|
24616
24616
|
content: [
|
|
24617
|
-
`
|
|
24618
|
-
session.currentTask ? `
|
|
24617
|
+
`Paused work on "${session.cardTitle}".`,
|
|
24618
|
+
session.currentTask ? `Last task: ${session.currentTask}` : "",
|
|
24619
24619
|
session.progressPercent !== undefined ? `Progress: ${session.progressPercent}%` : "",
|
|
24620
24620
|
durationInfo,
|
|
24621
|
-
session.
|
|
24622
|
-
session.blockers?.length ? `Blockers encountered: ${session.blockers.join("; ")}` : "",
|
|
24621
|
+
session.blockers?.length ? `Blockers: ${session.blockers.join("; ")}` : "",
|
|
24623
24622
|
`
|
|
24624
24623
|
Agent: ${session.agentName}`,
|
|
24625
24624
|
wikiLinksLine
|
|
24626
24625
|
].filter(Boolean).join(`
|
|
24627
24626
|
`),
|
|
24628
24627
|
type: "lesson",
|
|
24629
|
-
tier: "
|
|
24630
|
-
confidence: 0.
|
|
24628
|
+
tier: "draft",
|
|
24629
|
+
confidence: 0.6,
|
|
24631
24630
|
tags: [
|
|
24632
24631
|
"auto-extracted",
|
|
24633
|
-
"session-
|
|
24632
|
+
"session-paused",
|
|
24634
24633
|
...session.cardLabels.slice(0, 3)
|
|
24635
24634
|
],
|
|
24636
24635
|
metadata: {
|
|
@@ -24639,35 +24638,14 @@ Agent: ${session.agentName}`,
|
|
|
24639
24638
|
}
|
|
24640
24639
|
});
|
|
24641
24640
|
}
|
|
24642
|
-
const hasBugLabel = session.cardLabels.some((l) => ["bug", "fix", "hotfix", "defect", "error"].includes(l.toLowerCase()));
|
|
24643
|
-
if (hasBugLabel && session.status === "completed") {
|
|
24644
|
-
learnings.push({
|
|
24645
|
-
title: `Solution: ${session.cardTitle}`,
|
|
24646
|
-
content: [
|
|
24647
|
-
`Resolved bug: "${session.cardTitle}"`,
|
|
24648
|
-
session.currentTask ? `
|
|
24649
|
-
Approach: ${session.currentTask}` : "",
|
|
24650
|
-
`
|
|
24651
|
-
Agent: ${session.agentName}`,
|
|
24652
|
-
wikiLinksLine
|
|
24653
|
-
].filter(Boolean).join(`
|
|
24654
|
-
`),
|
|
24655
|
-
type: "solution",
|
|
24656
|
-
tier: "reference",
|
|
24657
|
-
confidence: 0.8,
|
|
24658
|
-
tags: ["auto-extracted", "bug-fix", ...session.cardLabels.slice(0, 3)],
|
|
24659
|
-
metadata: {
|
|
24660
|
-
source: "active_learning",
|
|
24661
|
-
card_id: session.cardId,
|
|
24662
|
-
auto_confidence: true
|
|
24663
|
-
}
|
|
24664
|
-
});
|
|
24665
|
-
}
|
|
24666
24641
|
const entityIds = [];
|
|
24667
24642
|
const stepHistory = sessionTaskHistory.get(session.cardId);
|
|
24668
|
-
const
|
|
24643
|
+
const MIN_PROCEDURE_STEPS = 5;
|
|
24644
|
+
const MIN_PROCEDURE_DURATION_MS = 10 * 60 * 1000;
|
|
24645
|
+
const hasEnoughSteps = stepHistory && stepHistory.steps.length >= MIN_PROCEDURE_STEPS;
|
|
24646
|
+
const hasMinDuration = (session.sessionDurationMs ?? 0) >= MIN_PROCEDURE_DURATION_MS;
|
|
24669
24647
|
const isSuccessful = session.status === "completed" && (session.progressPercent === undefined || session.progressPercent >= 85) && !session.blockers?.length;
|
|
24670
|
-
if (isSuccessful && hasEnoughSteps) {
|
|
24648
|
+
if (isSuccessful && hasEnoughSteps && hasMinDuration) {
|
|
24671
24649
|
const procedureResult = await extractOrReinforceProcedure(client2, session, stepHistory.steps, workspaceId, projectId, wikiLinksLine);
|
|
24672
24650
|
if (procedureResult) {
|
|
24673
24651
|
if (procedureResult.mode === "created") {
|
|
@@ -24711,218 +24689,9 @@ Agent: ${session.agentName}`,
|
|
|
24711
24689
|
if (createdPairs.length >= 2) {
|
|
24712
24690
|
linkSessionEntities(client2, createdPairs, workspaceId, projectId).catch(() => {});
|
|
24713
24691
|
}
|
|
24714
|
-
if (entityIds.length > 0) {
|
|
24715
|
-
detectAndCreatePatterns(client2, entityIds, session, workspaceId, projectId).catch(() => {});
|
|
24716
|
-
}
|
|
24717
|
-
if (createdPairs.length > 0) {
|
|
24718
|
-
detectCausalPatterns(client2, createdPairs, session, workspaceId, projectId).catch(() => {});
|
|
24719
|
-
}
|
|
24720
24692
|
clearMidSessionTracking(session.cardId);
|
|
24721
24693
|
return { count: entityIds.length, entityIds };
|
|
24722
24694
|
}
|
|
24723
|
-
var PATTERN_THRESHOLD = 3;
|
|
24724
|
-
async function detectAndCreatePatterns(client2, newEntityIds, session, workspaceId, projectId) {
|
|
24725
|
-
const patternEntityIds = [];
|
|
24726
|
-
for (const newEntityId of newEntityIds) {
|
|
24727
|
-
try {
|
|
24728
|
-
const { entity: rawEntity } = await client2.getMemoryEntity(newEntityId);
|
|
24729
|
-
const entity = rawEntity;
|
|
24730
|
-
if (!entity?.type)
|
|
24731
|
-
continue;
|
|
24732
|
-
const similar = await findSimilarEntities(client2, entity.title, entity.content, workspaceId, { projectId, limit: 30, minRrfScore: 0.01 });
|
|
24733
|
-
const existing = similar.filter((c) => !newEntityIds.includes(c.id) && c.type === entity.type);
|
|
24734
|
-
if (existing.length < PATTERN_THRESHOLD)
|
|
24735
|
-
continue;
|
|
24736
|
-
const memberTitles = [
|
|
24737
|
-
entity.title,
|
|
24738
|
-
...existing.slice(0, 4).map((e) => e.title)
|
|
24739
|
-
];
|
|
24740
|
-
const patternTitle = `Pattern: recurring ${entity.type} (${existing.length + 1} instances)`;
|
|
24741
|
-
const { entities: existingPatterns } = await client2.listMemoryEntities({
|
|
24742
|
-
workspace_id: workspaceId,
|
|
24743
|
-
project_id: projectId,
|
|
24744
|
-
type: "pattern",
|
|
24745
|
-
limit: 10
|
|
24746
|
-
});
|
|
24747
|
-
const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_type === entity.type);
|
|
24748
|
-
let patternId = null;
|
|
24749
|
-
if (matchingPattern) {
|
|
24750
|
-
patternId = matchingPattern.id;
|
|
24751
|
-
await client2.updateMemoryEntity(patternId, {
|
|
24752
|
-
content: `Recurring pattern: ${entity.type} entities appearing ${existing.length + 1} times.
|
|
24753
|
-
|
|
24754
|
-
Members:
|
|
24755
|
-
${memberTitles.map((t) => `- ${t}`).join(`
|
|
24756
|
-
`)}
|
|
24757
|
-
|
|
24758
|
-
Last updated: ${new Date().toISOString()}`,
|
|
24759
|
-
metadata: {
|
|
24760
|
-
pattern_count: existing.length + 1,
|
|
24761
|
-
pattern_type: entity.type,
|
|
24762
|
-
last_updated: new Date().toISOString()
|
|
24763
|
-
}
|
|
24764
|
-
});
|
|
24765
|
-
} else {
|
|
24766
|
-
const result = await client2.createMemoryEntity({
|
|
24767
|
-
workspace_id: workspaceId,
|
|
24768
|
-
project_id: projectId,
|
|
24769
|
-
type: "pattern",
|
|
24770
|
-
scope: "project",
|
|
24771
|
-
memory_tier: "reference",
|
|
24772
|
-
title: patternTitle,
|
|
24773
|
-
content: `Recurring pattern: ${entity.type} entities detected ${existing.length + 1} times.
|
|
24774
|
-
|
|
24775
|
-
Members:
|
|
24776
|
-
${memberTitles.map((t) => `- ${t}`).join(`
|
|
24777
|
-
`)}`,
|
|
24778
|
-
confidence: 0.75,
|
|
24779
|
-
tags: ["auto-extracted", "pattern", entity.type],
|
|
24780
|
-
metadata: {
|
|
24781
|
-
source: "pattern_detection",
|
|
24782
|
-
pattern_type: entity.type,
|
|
24783
|
-
pattern_count: existing.length + 1
|
|
24784
|
-
},
|
|
24785
|
-
agent_identifier: session.agentIdentifier
|
|
24786
|
-
});
|
|
24787
|
-
const created = result.entity;
|
|
24788
|
-
if (created?.id) {
|
|
24789
|
-
patternId = created.id;
|
|
24790
|
-
patternEntityIds.push(patternId);
|
|
24791
|
-
}
|
|
24792
|
-
}
|
|
24793
|
-
if (!patternId)
|
|
24794
|
-
continue;
|
|
24795
|
-
const toLink = [newEntityId, ...existing.slice(0, 4).map((e) => e.id)];
|
|
24796
|
-
for (const sourceId of toLink) {
|
|
24797
|
-
try {
|
|
24798
|
-
await client2.createMemoryRelation({
|
|
24799
|
-
source_id: sourceId,
|
|
24800
|
-
target_id: patternId,
|
|
24801
|
-
relation_type: "part_of",
|
|
24802
|
-
confidence: 0.75
|
|
24803
|
-
});
|
|
24804
|
-
} catch {}
|
|
24805
|
-
}
|
|
24806
|
-
} catch {}
|
|
24807
|
-
}
|
|
24808
|
-
return patternEntityIds;
|
|
24809
|
-
}
|
|
24810
|
-
var CAUSAL_PATTERN_THRESHOLD = 3;
|
|
24811
|
-
async function detectCausalPatterns(client2, createdPairs, session, workspaceId, projectId) {
|
|
24812
|
-
const patternIds = [];
|
|
24813
|
-
const errors3 = createdPairs.filter((p) => p.learning.type === "error");
|
|
24814
|
-
const solutions = createdPairs.filter((p) => p.learning.type === "solution");
|
|
24815
|
-
if (errors3.length === 0 || solutions.length === 0)
|
|
24816
|
-
return patternIds;
|
|
24817
|
-
for (const errorPair of errors3) {
|
|
24818
|
-
try {
|
|
24819
|
-
const similarErrors = await findSimilarEntities(client2, errorPair.learning.title, errorPair.learning.content, workspaceId, {
|
|
24820
|
-
projectId,
|
|
24821
|
-
limit: 20,
|
|
24822
|
-
minRrfScore: 0.03,
|
|
24823
|
-
excludeIds: createdPairs.map((p) => p.id),
|
|
24824
|
-
type: "error"
|
|
24825
|
-
});
|
|
24826
|
-
const resolvedErrors = [];
|
|
24827
|
-
for (const similar of similarErrors.slice(0, 10)) {
|
|
24828
|
-
try {
|
|
24829
|
-
const { outgoing } = await client2.getRelatedEntities(similar.id);
|
|
24830
|
-
const resolvedByRel = outgoing.find((r) => r.relation_type === "resolved_by");
|
|
24831
|
-
if (resolvedByRel) {
|
|
24832
|
-
resolvedErrors.push({
|
|
24833
|
-
errorId: similar.id,
|
|
24834
|
-
errorTitle: similar.title,
|
|
24835
|
-
solutionTitle: resolvedByRel.target_title || "unknown"
|
|
24836
|
-
});
|
|
24837
|
-
}
|
|
24838
|
-
} catch {}
|
|
24839
|
-
}
|
|
24840
|
-
if (resolvedErrors.length + 1 < CAUSAL_PATTERN_THRESHOLD)
|
|
24841
|
-
continue;
|
|
24842
|
-
const { entities: existingPatterns } = await client2.listMemoryEntities({
|
|
24843
|
-
workspace_id: workspaceId,
|
|
24844
|
-
project_id: projectId,
|
|
24845
|
-
type: "pattern",
|
|
24846
|
-
limit: 10
|
|
24847
|
-
});
|
|
24848
|
-
const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_chain_type === "error_resolved_by_solution");
|
|
24849
|
-
if (matchingPattern) {
|
|
24850
|
-
await client2.updateMemoryEntity(matchingPattern.id, {
|
|
24851
|
-
content: [
|
|
24852
|
-
`Recurring error→solution chain detected (${resolvedErrors.length + 1} instances).`,
|
|
24853
|
-
"",
|
|
24854
|
-
"## Error→Solution Pairs",
|
|
24855
|
-
`- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
|
|
24856
|
-
...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`),
|
|
24857
|
-
"",
|
|
24858
|
-
`Last updated: ${new Date().toISOString()}`
|
|
24859
|
-
].join(`
|
|
24860
|
-
`),
|
|
24861
|
-
metadata: {
|
|
24862
|
-
pattern_chain_type: "error_resolved_by_solution",
|
|
24863
|
-
pattern_count: resolvedErrors.length + 1,
|
|
24864
|
-
last_updated: new Date().toISOString()
|
|
24865
|
-
}
|
|
24866
|
-
});
|
|
24867
|
-
for (const pair of [errorPair, solutions[0]]) {
|
|
24868
|
-
try {
|
|
24869
|
-
await client2.createMemoryRelation({
|
|
24870
|
-
source_id: pair.id,
|
|
24871
|
-
target_id: matchingPattern.id,
|
|
24872
|
-
relation_type: "part_of",
|
|
24873
|
-
confidence: 0.75
|
|
24874
|
-
});
|
|
24875
|
-
} catch {}
|
|
24876
|
-
}
|
|
24877
|
-
} else {
|
|
24878
|
-
const result = await client2.createMemoryEntity({
|
|
24879
|
-
workspace_id: workspaceId,
|
|
24880
|
-
project_id: projectId,
|
|
24881
|
-
type: "pattern",
|
|
24882
|
-
scope: "project",
|
|
24883
|
-
memory_tier: "reference",
|
|
24884
|
-
title: `Pattern: recurring error→solution chain (${resolvedErrors.length + 1} instances)`,
|
|
24885
|
-
content: [
|
|
24886
|
-
`Recurring error→solution chain detected across ${resolvedErrors.length + 1} sessions.`,
|
|
24887
|
-
"",
|
|
24888
|
-
"## Error→Solution Pairs",
|
|
24889
|
-
`- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
|
|
24890
|
-
...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`)
|
|
24891
|
-
].join(`
|
|
24892
|
-
`),
|
|
24893
|
-
confidence: 0.8,
|
|
24894
|
-
tags: ["auto-extracted", "pattern", "causal-chain"],
|
|
24895
|
-
metadata: {
|
|
24896
|
-
source: "causal_pattern_detection",
|
|
24897
|
-
pattern_chain_type: "error_resolved_by_solution",
|
|
24898
|
-
pattern_count: resolvedErrors.length + 1
|
|
24899
|
-
},
|
|
24900
|
-
agent_identifier: session.agentIdentifier
|
|
24901
|
-
});
|
|
24902
|
-
const created = result.entity;
|
|
24903
|
-
if (created?.id) {
|
|
24904
|
-
patternIds.push(created.id);
|
|
24905
|
-
const memberIds = [
|
|
24906
|
-
errorPair.id,
|
|
24907
|
-
solutions[0].id,
|
|
24908
|
-
...resolvedErrors.slice(0, 4).map((r) => r.errorId)
|
|
24909
|
-
];
|
|
24910
|
-
for (const memberId of memberIds) {
|
|
24911
|
-
try {
|
|
24912
|
-
await client2.createMemoryRelation({
|
|
24913
|
-
source_id: memberId,
|
|
24914
|
-
target_id: created.id,
|
|
24915
|
-
relation_type: "part_of",
|
|
24916
|
-
confidence: 0.75
|
|
24917
|
-
});
|
|
24918
|
-
} catch {}
|
|
24919
|
-
}
|
|
24920
|
-
}
|
|
24921
|
-
}
|
|
24922
|
-
} catch {}
|
|
24923
|
-
}
|
|
24924
|
-
return patternIds;
|
|
24925
|
-
}
|
|
24926
24695
|
async function detectContradictions(client2, entityId, entityType, title, content, tags, workspaceId, projectId) {
|
|
24927
24696
|
if (!CONTRADICTION_TYPES.has(entityType))
|
|
24928
24697
|
return [];
|
|
@@ -25735,7 +25504,7 @@ async function autoEndSession(client3, cardId, status) {
|
|
|
25735
25504
|
// src/consolidation.ts
|
|
25736
25505
|
async function consolidateMemories(client3, workspaceId, projectId, options) {
|
|
25737
25506
|
const dryRun = options?.dryRun !== false;
|
|
25738
|
-
const minClusterSize = options?.minClusterSize ??
|
|
25507
|
+
const minClusterSize = options?.minClusterSize ?? 3;
|
|
25739
25508
|
const result = {
|
|
25740
25509
|
consolidated: 0,
|
|
25741
25510
|
clustersFound: 0,
|
|
@@ -25800,12 +25569,7 @@ async function consolidateMemories(client3, workspaceId, projectId, options) {
|
|
|
25800
25569
|
result.clustersFound++;
|
|
25801
25570
|
const mergedTitle = deriveClusterTitle(cluster, type);
|
|
25802
25571
|
const memberTitles = cluster.map((e) => e.title);
|
|
25803
|
-
const mergedContent =
|
|
25804
|
-
`Consolidated from ${cluster.length} ${type} memories:
|
|
25805
|
-
`,
|
|
25806
|
-
...cluster.map((e) => `- **${e.title}**: ${e.content.slice(0, 200)}`)
|
|
25807
|
-
].join(`
|
|
25808
|
-
`);
|
|
25572
|
+
const mergedContent = synthesizeClusterContent(cluster, type);
|
|
25809
25573
|
const maxConfidence = Math.max(...cluster.map((e) => e.confidence));
|
|
25810
25574
|
const allTags = [...new Set(cluster.flatMap((e) => e.tags || []))];
|
|
25811
25575
|
const detail = {
|
|
@@ -25867,6 +25631,60 @@ async function consolidateMemories(client3, workspaceId, projectId, options) {
|
|
|
25867
25631
|
}
|
|
25868
25632
|
return result;
|
|
25869
25633
|
}
|
|
25634
|
+
function synthesizeClusterContent(cluster, type) {
|
|
25635
|
+
const SKIP_PATTERNS = [
|
|
25636
|
+
/^##\s/,
|
|
25637
|
+
/^Agent:/,
|
|
25638
|
+
/^Duration:/,
|
|
25639
|
+
/^Labels:/,
|
|
25640
|
+
/^Progress:/,
|
|
25641
|
+
/^Session status:/,
|
|
25642
|
+
/^Completed at/,
|
|
25643
|
+
/^Final state:/,
|
|
25644
|
+
/^Related:/,
|
|
25645
|
+
/^When working on:/,
|
|
25646
|
+
/^\d+\.\s+.+\(\d+%,\s*\+\d+%\)/,
|
|
25647
|
+
/^Last updated:/,
|
|
25648
|
+
/^Recurring pattern:/,
|
|
25649
|
+
/^Consolidated from/
|
|
25650
|
+
];
|
|
25651
|
+
const seenLines = new Set;
|
|
25652
|
+
const knowledgeLines = [];
|
|
25653
|
+
for (const entity of cluster) {
|
|
25654
|
+
const lines = entity.content.split(`
|
|
25655
|
+
`).map((l) => l.trim());
|
|
25656
|
+
for (const line of lines) {
|
|
25657
|
+
if (!line || line.length < 20)
|
|
25658
|
+
continue;
|
|
25659
|
+
if (SKIP_PATTERNS.some((p) => p.test(line)))
|
|
25660
|
+
continue;
|
|
25661
|
+
const normalized = line.toLowerCase().replace(/[*_`#[\]]/g, "").trim();
|
|
25662
|
+
if (seenLines.has(normalized))
|
|
25663
|
+
continue;
|
|
25664
|
+
seenLines.add(normalized);
|
|
25665
|
+
knowledgeLines.push(line);
|
|
25666
|
+
}
|
|
25667
|
+
}
|
|
25668
|
+
if (knowledgeLines.length === 0) {
|
|
25669
|
+
return `${cluster.length} related ${type} entities consolidated. Original titles:
|
|
25670
|
+
${cluster.map((e) => `- ${e.title}`).join(`
|
|
25671
|
+
`)}`;
|
|
25672
|
+
}
|
|
25673
|
+
const MAX_CHARS = 1600;
|
|
25674
|
+
const result = [
|
|
25675
|
+
`Consolidated knowledge from ${cluster.length} ${type} entities:
|
|
25676
|
+
`
|
|
25677
|
+
];
|
|
25678
|
+
let charCount = result[0].length;
|
|
25679
|
+
for (const line of knowledgeLines) {
|
|
25680
|
+
if (charCount + line.length + 3 > MAX_CHARS)
|
|
25681
|
+
break;
|
|
25682
|
+
result.push(`- ${line}`);
|
|
25683
|
+
charCount += line.length + 3;
|
|
25684
|
+
}
|
|
25685
|
+
return result.join(`
|
|
25686
|
+
`);
|
|
25687
|
+
}
|
|
25870
25688
|
function deriveClusterTitle(cluster, type) {
|
|
25871
25689
|
const stopWords = new Set([
|
|
25872
25690
|
"the",
|
|
@@ -25921,9 +25739,9 @@ function deriveClusterTitle(cluster, type) {
|
|
|
25921
25739
|
wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
|
|
25922
25740
|
}
|
|
25923
25741
|
}
|
|
25924
|
-
const topWords = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0,
|
|
25925
|
-
const suffix = topWords.length > 0 ? topWords.join("
|
|
25926
|
-
return
|
|
25742
|
+
const topWords = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 4).map(([word]) => word[0].toUpperCase() + word.slice(1));
|
|
25743
|
+
const suffix = topWords.length > 0 ? topWords.join(" / ") : "Various";
|
|
25744
|
+
return `${type[0].toUpperCase() + type.slice(1)}: ${suffix}`;
|
|
25927
25745
|
}
|
|
25928
25746
|
|
|
25929
25747
|
// src/server.ts
|
|
@@ -25999,6 +25817,371 @@ async function runLifecycleMaintenance(client3, workspaceId, projectId) {
|
|
|
25999
25817
|
return result;
|
|
26000
25818
|
}
|
|
26001
25819
|
|
|
25820
|
+
// src/memory-cleanup.ts
|
|
25821
|
+
init_dist();
|
|
25822
|
+
var ALL_STEPS = [
|
|
25823
|
+
"prune",
|
|
25824
|
+
"consolidate",
|
|
25825
|
+
"orphans",
|
|
25826
|
+
"duplicates",
|
|
25827
|
+
"backfill"
|
|
25828
|
+
];
|
|
25829
|
+
async function runMemoryCleanup(client3, workspaceId, projectId, options) {
|
|
25830
|
+
const dryRun = options?.dryRun !== false;
|
|
25831
|
+
const steps = options?.steps ?? ALL_STEPS;
|
|
25832
|
+
const maxAgeDays = options?.maxAgeDays ?? 30;
|
|
25833
|
+
const minClusterSize = options?.minClusterSize ?? 3;
|
|
25834
|
+
const orphanAgeDays = options?.orphanAgeDays ?? 14;
|
|
25835
|
+
const report = {
|
|
25836
|
+
success: true,
|
|
25837
|
+
dryRun,
|
|
25838
|
+
timestamp: new Date().toISOString(),
|
|
25839
|
+
workspace: { id: workspaceId, projectId },
|
|
25840
|
+
summary: { totalEntities: 0, issuesFound: 0, actionsTaken: 0 },
|
|
25841
|
+
steps: {},
|
|
25842
|
+
errors: [],
|
|
25843
|
+
healthReport: ""
|
|
25844
|
+
};
|
|
25845
|
+
let entities = [];
|
|
25846
|
+
try {
|
|
25847
|
+
const listResult = await client3.listMemoryEntities({
|
|
25848
|
+
workspace_id: workspaceId,
|
|
25849
|
+
project_id: projectId,
|
|
25850
|
+
limit: 200
|
|
25851
|
+
});
|
|
25852
|
+
entities = listResult.entities || [];
|
|
25853
|
+
report.summary.totalEntities = entities.length;
|
|
25854
|
+
} catch (err) {
|
|
25855
|
+
report.errors.push({
|
|
25856
|
+
step: "init",
|
|
25857
|
+
message: `Failed to fetch entities: ${err.message}`
|
|
25858
|
+
});
|
|
25859
|
+
report.success = false;
|
|
25860
|
+
report.healthReport = generateHealthReport(report);
|
|
25861
|
+
return report;
|
|
25862
|
+
}
|
|
25863
|
+
if (steps.includes("prune")) {
|
|
25864
|
+
try {
|
|
25865
|
+
report.steps.prune = runPruneStep(entities, maxAgeDays);
|
|
25866
|
+
if (!dryRun) {
|
|
25867
|
+
for (const item of report.steps.prune.items) {
|
|
25868
|
+
try {
|
|
25869
|
+
await client3.deleteMemoryEntity(item.id);
|
|
25870
|
+
report.steps.prune.pruned++;
|
|
25871
|
+
} catch {}
|
|
25872
|
+
}
|
|
25873
|
+
report.summary.actionsTaken += report.steps.prune.pruned;
|
|
25874
|
+
}
|
|
25875
|
+
report.summary.issuesFound += report.steps.prune.staleDraftsFound;
|
|
25876
|
+
} catch (err) {
|
|
25877
|
+
report.errors.push({
|
|
25878
|
+
step: "prune",
|
|
25879
|
+
message: err.message
|
|
25880
|
+
});
|
|
25881
|
+
}
|
|
25882
|
+
}
|
|
25883
|
+
if (steps.includes("consolidate")) {
|
|
25884
|
+
try {
|
|
25885
|
+
const result = await consolidateMemories(client3, workspaceId, projectId, {
|
|
25886
|
+
dryRun,
|
|
25887
|
+
minClusterSize
|
|
25888
|
+
});
|
|
25889
|
+
report.steps.consolidate = {
|
|
25890
|
+
clustersFound: result.clustersFound,
|
|
25891
|
+
entitiesProcessed: result.entitiesProcessed,
|
|
25892
|
+
consolidated: result.consolidated,
|
|
25893
|
+
details: result.details
|
|
25894
|
+
};
|
|
25895
|
+
report.summary.issuesFound += result.clustersFound;
|
|
25896
|
+
if (!dryRun)
|
|
25897
|
+
report.summary.actionsTaken += result.consolidated;
|
|
25898
|
+
} catch (err) {
|
|
25899
|
+
report.errors.push({
|
|
25900
|
+
step: "consolidate",
|
|
25901
|
+
message: err.message
|
|
25902
|
+
});
|
|
25903
|
+
}
|
|
25904
|
+
}
|
|
25905
|
+
if (steps.includes("orphans")) {
|
|
25906
|
+
try {
|
|
25907
|
+
report.steps.orphans = await runOrphanStep(client3, entities, orphanAgeDays);
|
|
25908
|
+
if (!dryRun) {
|
|
25909
|
+
for (const item of report.steps.orphans.items) {
|
|
25910
|
+
try {
|
|
25911
|
+
await client3.deleteMemoryEntity(item.id);
|
|
25912
|
+
report.steps.orphans.removed++;
|
|
25913
|
+
} catch {}
|
|
25914
|
+
}
|
|
25915
|
+
report.summary.actionsTaken += report.steps.orphans.removed;
|
|
25916
|
+
}
|
|
25917
|
+
report.summary.issuesFound += report.steps.orphans.orphansFound;
|
|
25918
|
+
} catch (err) {
|
|
25919
|
+
report.errors.push({
|
|
25920
|
+
step: "orphans",
|
|
25921
|
+
message: err.message
|
|
25922
|
+
});
|
|
25923
|
+
}
|
|
25924
|
+
}
|
|
25925
|
+
if (steps.includes("duplicates")) {
|
|
25926
|
+
try {
|
|
25927
|
+
report.steps.duplicates = await runDuplicateStep(client3, entities, workspaceId, projectId);
|
|
25928
|
+
if (!dryRun) {
|
|
25929
|
+
for (const pair of report.steps.duplicates.pairs) {
|
|
25930
|
+
try {
|
|
25931
|
+
await client3.deleteMemoryEntity(pair.removeId);
|
|
25932
|
+
report.steps.duplicates.resolved++;
|
|
25933
|
+
} catch {}
|
|
25934
|
+
}
|
|
25935
|
+
report.summary.actionsTaken += report.steps.duplicates.resolved;
|
|
25936
|
+
}
|
|
25937
|
+
report.summary.issuesFound += report.steps.duplicates.duplicatePairsFound;
|
|
25938
|
+
} catch (err) {
|
|
25939
|
+
report.errors.push({
|
|
25940
|
+
step: "duplicates",
|
|
25941
|
+
message: err.message
|
|
25942
|
+
});
|
|
25943
|
+
}
|
|
25944
|
+
}
|
|
25945
|
+
if (steps.includes("backfill")) {
|
|
25946
|
+
try {
|
|
25947
|
+
if (dryRun) {
|
|
25948
|
+
report.steps.backfill = {
|
|
25949
|
+
processed: 0,
|
|
25950
|
+
remaining: -1,
|
|
25951
|
+
errors: []
|
|
25952
|
+
};
|
|
25953
|
+
} else {
|
|
25954
|
+
const result = await client3.backfillEmbeddings(workspaceId);
|
|
25955
|
+
report.steps.backfill = {
|
|
25956
|
+
processed: result.processed,
|
|
25957
|
+
remaining: result.remaining,
|
|
25958
|
+
errors: result.errors || []
|
|
25959
|
+
};
|
|
25960
|
+
report.summary.actionsTaken += result.processed;
|
|
25961
|
+
}
|
|
25962
|
+
} catch (err) {
|
|
25963
|
+
report.errors.push({
|
|
25964
|
+
step: "backfill",
|
|
25965
|
+
message: err.message
|
|
25966
|
+
});
|
|
25967
|
+
}
|
|
25968
|
+
}
|
|
25969
|
+
report.healthReport = generateHealthReport(report);
|
|
25970
|
+
return report;
|
|
25971
|
+
}
|
|
25972
|
+
function runPruneStep(entities, maxAgeDays) {
|
|
25973
|
+
const now = Date.now();
|
|
25974
|
+
const drafts = entities.filter((e) => e.memory_tier === "draft");
|
|
25975
|
+
const stale = [];
|
|
25976
|
+
for (const entity of drafts) {
|
|
25977
|
+
const ageDays = (now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
25978
|
+
if (ageDays < maxAgeDays)
|
|
25979
|
+
continue;
|
|
25980
|
+
const lifecycle2 = evaluateLifecycle(entity);
|
|
25981
|
+
stale.push({
|
|
25982
|
+
id: entity.id,
|
|
25983
|
+
title: entity.title,
|
|
25984
|
+
ageDays: Math.round(ageDays),
|
|
25985
|
+
decayScore: Math.round(lifecycle2.decay.score * 100) / 100
|
|
25986
|
+
});
|
|
25987
|
+
}
|
|
25988
|
+
return { staleDraftsFound: stale.length, pruned: 0, items: stale };
|
|
25989
|
+
}
|
|
25990
|
+
async function runOrphanStep(client3, entities, orphanAgeDays) {
|
|
25991
|
+
const now = Date.now();
|
|
25992
|
+
const result = { orphansFound: 0, removed: 0, items: [] };
|
|
25993
|
+
const candidates = entities.filter((e) => {
|
|
25994
|
+
if (e.memory_tier === "reference")
|
|
25995
|
+
return false;
|
|
25996
|
+
if (e.access_count >= 2)
|
|
25997
|
+
return false;
|
|
25998
|
+
const ageDays = (now - new Date(e.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
25999
|
+
return ageDays >= orphanAgeDays;
|
|
26000
|
+
});
|
|
26001
|
+
for (const entity of candidates) {
|
|
26002
|
+
try {
|
|
26003
|
+
const related = await client3.getRelatedEntities(entity.id);
|
|
26004
|
+
const totalRelations = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
|
|
26005
|
+
if (totalRelations > 0)
|
|
26006
|
+
continue;
|
|
26007
|
+
const ageDays = (now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
26008
|
+
result.items.push({
|
|
26009
|
+
id: entity.id,
|
|
26010
|
+
title: entity.title,
|
|
26011
|
+
type: entity.type,
|
|
26012
|
+
tier: entity.memory_tier,
|
|
26013
|
+
ageDays: Math.round(ageDays),
|
|
26014
|
+
accessCount: entity.access_count
|
|
26015
|
+
});
|
|
26016
|
+
result.orphansFound++;
|
|
26017
|
+
} catch {}
|
|
26018
|
+
}
|
|
26019
|
+
return result;
|
|
26020
|
+
}
|
|
26021
|
+
async function runDuplicateStep(client3, entities, workspaceId, projectId) {
|
|
26022
|
+
const result = {
|
|
26023
|
+
duplicatePairsFound: 0,
|
|
26024
|
+
resolved: 0,
|
|
26025
|
+
pairs: []
|
|
26026
|
+
};
|
|
26027
|
+
const seenPairs = new Set;
|
|
26028
|
+
const flaggedForRemoval = new Set;
|
|
26029
|
+
for (const entity of entities) {
|
|
26030
|
+
if (flaggedForRemoval.has(entity.id))
|
|
26031
|
+
continue;
|
|
26032
|
+
let similar;
|
|
26033
|
+
try {
|
|
26034
|
+
similar = await findSimilarEntities(client3, entity.title, entity.content, workspaceId, { projectId, limit: 5, minRrfScore: 0.05, excludeIds: [entity.id] });
|
|
26035
|
+
} catch {
|
|
26036
|
+
continue;
|
|
26037
|
+
}
|
|
26038
|
+
for (const match of similar) {
|
|
26039
|
+
if (flaggedForRemoval.has(match.id))
|
|
26040
|
+
continue;
|
|
26041
|
+
const pairKey = [entity.id, match.id].sort().join(":");
|
|
26042
|
+
if (seenPairs.has(pairKey))
|
|
26043
|
+
continue;
|
|
26044
|
+
seenPairs.add(pairKey);
|
|
26045
|
+
const sim = titleSimilarity(entity.title, match.title);
|
|
26046
|
+
if (sim < 0.85)
|
|
26047
|
+
continue;
|
|
26048
|
+
const entityScore = entityQualityScore(entity);
|
|
26049
|
+
const matchEntity = entities.find((e) => e.id === match.id);
|
|
26050
|
+
const matchScore = matchEntity ? entityQualityScore(matchEntity) : match.confidence;
|
|
26051
|
+
const [keep, remove] = entityScore >= matchScore ? [entity, { id: match.id, title: match.title }] : [{ id: match.id, title: match.title }, entity];
|
|
26052
|
+
flaggedForRemoval.add(remove.id);
|
|
26053
|
+
result.pairs.push({
|
|
26054
|
+
keepId: keep.id,
|
|
26055
|
+
keepTitle: keep.title,
|
|
26056
|
+
removeId: remove.id,
|
|
26057
|
+
removeTitle: remove.title,
|
|
26058
|
+
similarity: Math.round(sim * 100) / 100
|
|
26059
|
+
});
|
|
26060
|
+
result.duplicatePairsFound++;
|
|
26061
|
+
}
|
|
26062
|
+
}
|
|
26063
|
+
return result;
|
|
26064
|
+
}
|
|
26065
|
+
var TIER_WEIGHTS2 = {
|
|
26066
|
+
reference: 3,
|
|
26067
|
+
episode: 2,
|
|
26068
|
+
draft: 1
|
|
26069
|
+
};
|
|
26070
|
+
function entityQualityScore(entity) {
|
|
26071
|
+
return entity.confidence + (TIER_WEIGHTS2[entity.memory_tier] || 0) + Math.min(entity.access_count, 10) * 0.1;
|
|
26072
|
+
}
|
|
26073
|
+
function titleSimilarity(a, b) {
|
|
26074
|
+
const na = a.toLowerCase().trim();
|
|
26075
|
+
const nb = b.toLowerCase().trim();
|
|
26076
|
+
if (na === nb)
|
|
26077
|
+
return 1;
|
|
26078
|
+
const wordsA = new Set(na.split(/\W+/).filter(Boolean));
|
|
26079
|
+
const wordsB = new Set(nb.split(/\W+/).filter(Boolean));
|
|
26080
|
+
if (wordsA.size === 0 || wordsB.size === 0)
|
|
26081
|
+
return 0;
|
|
26082
|
+
let intersection3 = 0;
|
|
26083
|
+
for (const w of wordsA) {
|
|
26084
|
+
if (wordsB.has(w))
|
|
26085
|
+
intersection3++;
|
|
26086
|
+
}
|
|
26087
|
+
const union3 = wordsA.size + wordsB.size - intersection3;
|
|
26088
|
+
return union3 > 0 ? intersection3 / union3 : 0;
|
|
26089
|
+
}
|
|
26090
|
+
function generateHealthReport(report) {
|
|
26091
|
+
const mode = report.dryRun ? "Dry Run (preview)" : "Executed";
|
|
26092
|
+
const lines = [
|
|
26093
|
+
`# Memory Health Report
|
|
26094
|
+
`,
|
|
26095
|
+
`**Mode:** ${mode} | **Entities:** ${report.summary.totalEntities} | **Issues:** ${report.summary.issuesFound} | **Actions:** ${report.summary.actionsTaken}`,
|
|
26096
|
+
""
|
|
26097
|
+
];
|
|
26098
|
+
if (report.steps.prune) {
|
|
26099
|
+
const p = report.steps.prune;
|
|
26100
|
+
lines.push("## Stale Drafts");
|
|
26101
|
+
if (p.staleDraftsFound === 0) {
|
|
26102
|
+
lines.push(`No stale drafts found.
|
|
26103
|
+
`);
|
|
26104
|
+
} else {
|
|
26105
|
+
lines.push(`Found **${p.staleDraftsFound}** stale drafts${!report.dryRun ? ` (pruned ${p.pruned})` : ""}:`);
|
|
26106
|
+
lines.push("| Title | Age | Decay |");
|
|
26107
|
+
lines.push("|-------|-----|-------|");
|
|
26108
|
+
for (const item of p.items.slice(0, 20)) {
|
|
26109
|
+
lines.push(`| ${item.title} | ${item.ageDays}d | ${item.decayScore} |`);
|
|
26110
|
+
}
|
|
26111
|
+
lines.push("");
|
|
26112
|
+
}
|
|
26113
|
+
}
|
|
26114
|
+
if (report.steps.consolidate) {
|
|
26115
|
+
const c = report.steps.consolidate;
|
|
26116
|
+
lines.push("## Consolidation");
|
|
26117
|
+
if (c.clustersFound === 0) {
|
|
26118
|
+
lines.push(`Scanned ${c.entitiesProcessed} draft/episode entities — no clusters found.
|
|
26119
|
+
`);
|
|
26120
|
+
} else {
|
|
26121
|
+
lines.push(`Found **${c.clustersFound}** clusters across ${c.entitiesProcessed} entities:`);
|
|
26122
|
+
for (const d of c.details.slice(0, 10)) {
|
|
26123
|
+
lines.push(`- **${d.mergedTitle}** — ${d.clusterSize} entities`);
|
|
26124
|
+
}
|
|
26125
|
+
lines.push("");
|
|
26126
|
+
}
|
|
26127
|
+
}
|
|
26128
|
+
if (report.steps.orphans) {
|
|
26129
|
+
const o = report.steps.orphans;
|
|
26130
|
+
lines.push("## Orphaned Entities");
|
|
26131
|
+
if (o.orphansFound === 0) {
|
|
26132
|
+
lines.push(`No orphans found.
|
|
26133
|
+
`);
|
|
26134
|
+
} else {
|
|
26135
|
+
lines.push(`Found **${o.orphansFound}** orphans${!report.dryRun ? ` (removed ${o.removed})` : ""}:`);
|
|
26136
|
+
lines.push("| Title | Type | Tier | Age | Accesses |");
|
|
26137
|
+
lines.push("|-------|------|------|-----|----------|");
|
|
26138
|
+
for (const item of o.items.slice(0, 20)) {
|
|
26139
|
+
lines.push(`| ${item.title} | ${item.type} | ${item.tier} | ${item.ageDays}d | ${item.accessCount} |`);
|
|
26140
|
+
}
|
|
26141
|
+
lines.push("");
|
|
26142
|
+
}
|
|
26143
|
+
}
|
|
26144
|
+
if (report.steps.duplicates) {
|
|
26145
|
+
const d = report.steps.duplicates;
|
|
26146
|
+
lines.push("## Near-Duplicates");
|
|
26147
|
+
if (d.duplicatePairsFound === 0) {
|
|
26148
|
+
lines.push(`No duplicates found.
|
|
26149
|
+
`);
|
|
26150
|
+
} else {
|
|
26151
|
+
lines.push(`Found **${d.duplicatePairsFound}** duplicate pairs${!report.dryRun ? ` (resolved ${d.resolved})` : ""}:`);
|
|
26152
|
+
for (const pair of d.pairs.slice(0, 20)) {
|
|
26153
|
+
lines.push(`- "${pair.keepTitle}" ~ "${pair.removeTitle}" (${Math.round(pair.similarity * 100)}% similar, keep first)`);
|
|
26154
|
+
}
|
|
26155
|
+
lines.push("");
|
|
26156
|
+
}
|
|
26157
|
+
}
|
|
26158
|
+
if (report.steps.backfill) {
|
|
26159
|
+
const b = report.steps.backfill;
|
|
26160
|
+
lines.push("## Embedding Coverage");
|
|
26161
|
+
if (report.dryRun) {
|
|
26162
|
+
lines.push("Backfill will run when executed with `dryRun: false`.\n");
|
|
26163
|
+
} else if (b.remaining === 0) {
|
|
26164
|
+
lines.push(`All embeddings up to date (processed ${b.processed}).
|
|
26165
|
+
`);
|
|
26166
|
+
} else {
|
|
26167
|
+
lines.push(`Processed ${b.processed} entities. ${b.remaining} still need embeddings.
|
|
26168
|
+
`);
|
|
26169
|
+
}
|
|
26170
|
+
}
|
|
26171
|
+
if (report.errors.length > 0) {
|
|
26172
|
+
lines.push("## Errors");
|
|
26173
|
+
for (const e of report.errors) {
|
|
26174
|
+
lines.push(`- **${e.step}:** ${e.message}`);
|
|
26175
|
+
}
|
|
26176
|
+
lines.push("");
|
|
26177
|
+
}
|
|
26178
|
+
if (report.dryRun) {
|
|
26179
|
+
lines.push("---\n*Run with `dryRun: false` to execute cleanup.*");
|
|
26180
|
+
}
|
|
26181
|
+
return lines.join(`
|
|
26182
|
+
`);
|
|
26183
|
+
}
|
|
26184
|
+
|
|
26002
26185
|
// src/onboard.ts
|
|
26003
26186
|
async function onboardNewUser(params) {
|
|
26004
26187
|
const {
|
|
@@ -27433,6 +27616,47 @@ var TOOLS = {
|
|
|
27433
27616
|
},
|
|
27434
27617
|
required: []
|
|
27435
27618
|
}
|
|
27619
|
+
},
|
|
27620
|
+
harmony_cleanup_memories: {
|
|
27621
|
+
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.",
|
|
27622
|
+
inputSchema: {
|
|
27623
|
+
type: "object",
|
|
27624
|
+
properties: {
|
|
27625
|
+
workspaceId: {
|
|
27626
|
+
type: "string",
|
|
27627
|
+
description: "Workspace ID (optional if context set)"
|
|
27628
|
+
},
|
|
27629
|
+
projectId: {
|
|
27630
|
+
type: "string",
|
|
27631
|
+
description: "Project ID (optional)"
|
|
27632
|
+
},
|
|
27633
|
+
dryRun: {
|
|
27634
|
+
type: "boolean",
|
|
27635
|
+
description: "Preview cleanup without executing changes (default: true)"
|
|
27636
|
+
},
|
|
27637
|
+
steps: {
|
|
27638
|
+
type: "array",
|
|
27639
|
+
items: {
|
|
27640
|
+
type: "string",
|
|
27641
|
+
enum: ["prune", "consolidate", "orphans", "duplicates", "backfill"]
|
|
27642
|
+
},
|
|
27643
|
+
description: "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill."
|
|
27644
|
+
},
|
|
27645
|
+
maxAgeDays: {
|
|
27646
|
+
type: "number",
|
|
27647
|
+
description: "Max age in days for stale draft pruning (default: 30)"
|
|
27648
|
+
},
|
|
27649
|
+
minClusterSize: {
|
|
27650
|
+
type: "number",
|
|
27651
|
+
description: "Min cluster size for consolidation (default: 3)"
|
|
27652
|
+
},
|
|
27653
|
+
orphanAgeDays: {
|
|
27654
|
+
type: "number",
|
|
27655
|
+
description: "Min age in days for orphan detection (default: 14)"
|
|
27656
|
+
}
|
|
27657
|
+
},
|
|
27658
|
+
required: []
|
|
27659
|
+
}
|
|
27436
27660
|
}
|
|
27437
27661
|
};
|
|
27438
27662
|
var RESOURCES = [
|
|
@@ -28943,6 +29167,27 @@ async function handleToolCall(name, args, deps) {
|
|
|
28943
29167
|
message: `Processed ${entitiesProcessed} entities, created ${relationsCreated} new relations.`
|
|
28944
29168
|
};
|
|
28945
29169
|
}
|
|
29170
|
+
case "harmony_cleanup_memories": {
|
|
29171
|
+
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
|
29172
|
+
if (!workspaceId) {
|
|
29173
|
+
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
29174
|
+
}
|
|
29175
|
+
const projectId = args.projectId || deps.getActiveProjectId() || undefined;
|
|
29176
|
+
const report = await runMemoryCleanup(client3, workspaceId, projectId, {
|
|
29177
|
+
dryRun: args.dryRun,
|
|
29178
|
+
steps: args.steps,
|
|
29179
|
+
maxAgeDays: args.maxAgeDays,
|
|
29180
|
+
minClusterSize: args.minClusterSize,
|
|
29181
|
+
orphanAgeDays: args.orphanAgeDays
|
|
29182
|
+
});
|
|
29183
|
+
return {
|
|
29184
|
+
success: report.success,
|
|
29185
|
+
dryRun: report.dryRun,
|
|
29186
|
+
summary: report.summary,
|
|
29187
|
+
errors: report.errors,
|
|
29188
|
+
healthReport: report.healthReport
|
|
29189
|
+
};
|
|
29190
|
+
}
|
|
28946
29191
|
default:
|
|
28947
29192
|
throw new Error(`Unknown tool: ${name}`);
|
|
28948
29193
|
}
|