@gethmy/mcp 2.2.3 → 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 CHANGED
@@ -9163,6 +9163,7 @@ __export(exports_context_assembly, {
9163
9163
  mapToContextEntity: () => mapToContextEntity,
9164
9164
  getSessionAssemblyId: () => getSessionAssemblyId,
9165
9165
  getCachedManifest: () => getCachedManifest,
9166
+ expandQuery: () => expandQuery,
9166
9167
  computeRelevanceScore: () => computeRelevanceScore,
9167
9168
  cacheManifest: () => cacheManifest,
9168
9169
  assembleContext: () => assembleContext
@@ -9170,6 +9171,29 @@ __export(exports_context_assembly, {
9170
9171
  function estimateTokens(text) {
9171
9172
  return Math.ceil(text.length / 4);
9172
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
+ }
9173
9197
  function generateAssemblyId() {
9174
9198
  return `ctx_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
9175
9199
  }
@@ -9201,7 +9225,45 @@ function truncateContent(content, maxTokens) {
9201
9225
  }
9202
9226
  return { text: result, truncated: true };
9203
9227
  }
9204
- function computeRelevanceScore(entity, taskContext, cardLabels) {
9228
+ function escapeRegex2(str) {
9229
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9230
+ }
9231
+ function expandQuery(taskContext) {
9232
+ const queries = [taskContext];
9233
+ const lowerQueries = [taskContext.toLowerCase()];
9234
+ const words = taskContext.toLowerCase().split(/\W+/).filter((w) => w.length > 2);
9235
+ const expandableWords = words.filter((w) => QUERY_SYNONYMS[w]);
9236
+ for (const word of expandableWords) {
9237
+ const synonyms = QUERY_SYNONYMS[word];
9238
+ if (!synonyms)
9239
+ continue;
9240
+ const variation = taskContext.replace(new RegExp(`\\b${escapeRegex2(word)}\\b`, "gi"), synonyms[0]);
9241
+ const lowerVariation = variation.toLowerCase();
9242
+ if (lowerVariation !== taskContext.toLowerCase() && !lowerQueries.includes(lowerVariation)) {
9243
+ queries.push(variation);
9244
+ lowerQueries.push(lowerVariation);
9245
+ }
9246
+ if (queries.length >= MAX_QUERY_VARIATIONS)
9247
+ break;
9248
+ }
9249
+ if (words.length >= 3) {
9250
+ const keyPhrases = words.filter((w) => ![
9251
+ "the",
9252
+ "and",
9253
+ "for",
9254
+ "with",
9255
+ "this",
9256
+ "that",
9257
+ "from",
9258
+ "into"
9259
+ ].includes(w)).slice(0, 4).join(" ");
9260
+ if (!lowerQueries.includes(keyPhrases)) {
9261
+ queries.push(keyPhrases);
9262
+ }
9263
+ }
9264
+ return queries.slice(0, MAX_QUERY_VARIATIONS);
9265
+ }
9266
+ function computeRelevanceScore(entity, taskContext, cardLabels, graphRelations) {
9205
9267
  const reasons = [];
9206
9268
  let score = 0;
9207
9269
  const hasRrfScore = entity.rrf_score !== undefined && entity.rrf_score > 0;
@@ -9260,7 +9322,23 @@ function computeRelevanceScore(entity, taskContext, cardLabels) {
9260
9322
  score += 0.1;
9261
9323
  reasons.push("procedure_boost");
9262
9324
  }
9263
- score = Math.min(score, 1);
9325
+ if (graphRelations && graphRelations.length > 0) {
9326
+ const entityRelations = graphRelations.filter((r) => r.source_id === entity.id || r.target_id === entity.id);
9327
+ if (entityRelations.length > 0) {
9328
+ let bestBonus = 0;
9329
+ let bestRelType = "";
9330
+ for (const rel of entityRelations) {
9331
+ const bonus = RELATION_BONUSES[rel.relation_type] ?? 0.1;
9332
+ if (bonus > bestBonus) {
9333
+ bestBonus = bonus;
9334
+ bestRelType = rel.relation_type;
9335
+ }
9336
+ }
9337
+ score += bestBonus;
9338
+ reasons.push(`graph_walk(${bestRelType})`);
9339
+ }
9340
+ }
9341
+ score = Math.max(0, Math.min(score, 1));
9264
9342
  const tierWeight = TIER_WEIGHTS[entity.memory_tier];
9265
9343
  score *= tierWeight;
9266
9344
  return { score, reasons };
@@ -9272,7 +9350,11 @@ async function assembleContext(options) {
9272
9350
  taskContext,
9273
9351
  cardLabels = [],
9274
9352
  tokenBudget = DEFAULT_TOKEN_BUDGET,
9275
- client: client2
9353
+ client: client2,
9354
+ graphWalkEnabled = true,
9355
+ queryExpansionEnabled = true,
9356
+ enableLlmReranking = false,
9357
+ rerankFn
9276
9358
  } = options;
9277
9359
  const assemblyId = generateAssemblyId();
9278
9360
  const manifest = {
@@ -9288,13 +9370,26 @@ async function assembleContext(options) {
9288
9370
  reference: { count: 0, tokens: 0 }
9289
9371
  }
9290
9372
  };
9291
- let candidates = [];
9292
- try {
9293
- const searchResult = await client2.searchMemoryEntities(workspaceId, taskContext, { project_id: projectId, limit: 30 });
9294
- if (searchResult.entities?.length > 0) {
9295
- candidates = searchResult.entities.map(mapToContextEntity);
9373
+ const candidates = [];
9374
+ const queries = queryExpansionEnabled ? expandQuery(taskContext) : [taskContext];
9375
+ const searchResults = await Promise.allSettled(queries.map((query) => client2.searchMemoryEntities(workspaceId, query, {
9376
+ project_id: projectId,
9377
+ limit: 30
9378
+ })));
9379
+ const candidateIds = new Set;
9380
+ for (const result of searchResults) {
9381
+ if (result.status !== "fulfilled")
9382
+ continue;
9383
+ if (result.value.entities?.length > 0) {
9384
+ for (const raw of result.value.entities) {
9385
+ const entity = mapToContextEntity(raw);
9386
+ if (!candidateIds.has(entity.id)) {
9387
+ candidateIds.add(entity.id);
9388
+ candidates.push(entity);
9389
+ }
9390
+ }
9296
9391
  }
9297
- } catch {}
9392
+ }
9298
9393
  if (candidates.length < 10 && projectId) {
9299
9394
  try {
9300
9395
  const listResult = await client2.listMemoryEntities({
@@ -9303,9 +9398,13 @@ async function assembleContext(options) {
9303
9398
  limit: 30
9304
9399
  });
9305
9400
  if (listResult.entities?.length > 0) {
9306
- const existingIds = new Set(candidates.map((c) => c.id));
9307
- const additional = listResult.entities.map(mapToContextEntity).filter((e) => !existingIds.has(e.id));
9308
- candidates.push(...additional);
9401
+ for (const raw of listResult.entities) {
9402
+ const entity = mapToContextEntity(raw);
9403
+ if (!candidateIds.has(entity.id)) {
9404
+ candidateIds.add(entity.id);
9405
+ candidates.push(entity);
9406
+ }
9407
+ }
9309
9408
  }
9310
9409
  } catch {}
9311
9410
  }
@@ -9317,9 +9416,33 @@ async function assembleContext(options) {
9317
9416
  limit: 20
9318
9417
  });
9319
9418
  if (wsResult.entities?.length > 0) {
9320
- const existingIds = new Set(candidates.map((c) => c.id));
9321
- const additional = wsResult.entities.map(mapToContextEntity).filter((e) => !existingIds.has(e.id));
9322
- candidates.push(...additional);
9419
+ for (const raw of wsResult.entities) {
9420
+ const entity = mapToContextEntity(raw);
9421
+ if (!candidateIds.has(entity.id)) {
9422
+ candidateIds.add(entity.id);
9423
+ candidates.push(entity);
9424
+ }
9425
+ }
9426
+ }
9427
+ } catch {}
9428
+ }
9429
+ let graphRelations = [];
9430
+ if (graphWalkEnabled && candidates.length > 0) {
9431
+ try {
9432
+ const seedCandidates = [...candidates].sort((a, b) => (b.rrf_score ?? 0) - (a.rrf_score ?? 0)).slice(0, GRAPH_WALK_SEED_COUNT);
9433
+ const seedIds = seedCandidates.map((c) => c.id);
9434
+ const walkResult = await discoverRelatedContext(client2, seedIds, GRAPH_WALK_MAX_DEPTH, GRAPH_WALK_MAX_ENTITIES, GRAPH_WALK_MIN_CONFIDENCE);
9435
+ graphRelations = walkResult.relations;
9436
+ const newEntityIds = walkResult.entities.filter((e) => !candidateIds.has(e.id)).map((e) => e.id);
9437
+ if (newEntityIds.length > 0) {
9438
+ const fetchResults = await Promise.allSettled(newEntityIds.map((id) => client2.getMemoryEntity(id)));
9439
+ for (const result of fetchResults) {
9440
+ if (result.status !== "fulfilled" || !result.value.entity)
9441
+ continue;
9442
+ const mapped = mapToContextEntity(result.value.entity);
9443
+ candidateIds.add(mapped.id);
9444
+ candidates.push(mapped);
9445
+ }
9323
9446
  }
9324
9447
  } catch {}
9325
9448
  }
@@ -9330,11 +9453,52 @@ async function assembleContext(options) {
9330
9453
  memories: []
9331
9454
  };
9332
9455
  }
9333
- const scored = candidates.map((entity) => {
9334
- const { score, reasons } = computeRelevanceScore(entity, taskContext, cardLabels);
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) => {
9477
+ const { score, reasons } = computeRelevanceScore(entity, taskContext, cardLabels, graphRelations.length > 0 ? graphRelations : undefined);
9335
9478
  return { entity, score, reasons };
9336
9479
  });
9337
9480
  scored.sort((a, b) => b.score - a.score);
9481
+ if (enableLlmReranking && rerankFn && scored.length >= RERANK_MIN_CANDIDATES) {
9482
+ const topN = scored.slice(0, RERANK_TOP_N);
9483
+ const scoreRange = topN[0].score - topN[topN.length - 1].score;
9484
+ if (scoreRange <= RERANK_CLUSTER_THRESHOLD) {
9485
+ try {
9486
+ const rerankCandidates = topN.map((s) => ({
9487
+ id: s.entity.id,
9488
+ title: s.entity.title,
9489
+ snippet: s.entity.content.slice(0, 200)
9490
+ }));
9491
+ const rerankedIds = await rerankFn(taskContext, rerankCandidates);
9492
+ const idOrder = new Map(rerankedIds.map((id, i) => [id, i]));
9493
+ topN.sort((a, b) => {
9494
+ const aIdx = idOrder.get(a.entity.id) ?? 999;
9495
+ const bIdx = idOrder.get(b.entity.id) ?? 999;
9496
+ return aIdx - bIdx;
9497
+ });
9498
+ scored.splice(0, topN.length, ...topN);
9499
+ } catch {}
9500
+ }
9501
+ }
9338
9502
  const procedureBudget = Math.floor(tokenBudget * PROCEDURE_BUDGET_FRACTION);
9339
9503
  const remainingBudget = tokenBudget - procedureBudget;
9340
9504
  const tierBudgets = {
@@ -9646,7 +9810,7 @@ async function recordContextFeedback(client2, cardId, sessionStatus, progressPer
9646
9810
  sessionAssemblyMap.delete(cardId);
9647
9811
  return { adjusted };
9648
9812
  }
9649
- var DEFAULT_TOKEN_BUDGET = 4000, MAX_TOKENS_PER_ENTITY = 500, MIN_RELEVANCE_THRESHOLD = 0.1, TIER_WEIGHTS, PROCEDURE_BUDGET_FRACTION = 0.15, TIER_BUDGET_ALLOCATION, MIN_REFERENCE_SLOTS = 3, manifestCache, MAX_CACHE_SIZE = 50, sessionAssemblyMap, MAX_SESSION_MAP_SIZE = 100;
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;
9650
9814
  var init_context_assembly = __esm(() => {
9651
9815
  init_dist();
9652
9816
  TIER_WEIGHTS = {
@@ -9659,6 +9823,33 @@ var init_context_assembly = __esm(() => {
9659
9823
  episode: 0.3,
9660
9824
  draft: 0.1
9661
9825
  };
9826
+ RELATION_BONUSES = {
9827
+ depends_on: 0.15,
9828
+ resolved_by: 0.2,
9829
+ relates_to: 0.1,
9830
+ implements: 0.15,
9831
+ blocks: 0.15,
9832
+ references: 0.1,
9833
+ extends: 0.1,
9834
+ caused_by: 0.15
9835
+ };
9836
+ QUERY_SYNONYMS = {
9837
+ auth: ["authentication", "authorization", "session"],
9838
+ authentication: ["auth", "session", "sign-in"],
9839
+ login: ["sign-in", "authentication", "session"],
9840
+ bug: ["error", "issue", "defect", "problem"],
9841
+ error: ["exception", "failure", "issue"],
9842
+ fix: ["resolve", "patch", "repair", "correct"],
9843
+ deploy: ["deployment", "release", "ship", "publish"],
9844
+ test: ["testing", "spec", "assertion", "verify"],
9845
+ config: ["configuration", "settings", "setup"],
9846
+ db: ["database", "storage", "persistence"],
9847
+ database: ["storage", "persistence", "data store"],
9848
+ api: ["endpoint", "route", "service"],
9849
+ ui: ["frontend", "component", "view"],
9850
+ perf: ["performance", "speed", "latency"],
9851
+ performance: ["speed", "latency", "optimization"]
9852
+ };
9662
9853
  manifestCache = new Map;
9663
9854
  sessionAssemblyMap = new Map;
9664
9855
  });
@@ -9787,7 +9978,11 @@ ${card.description}`);
9787
9978
  roleFraming.focus.forEach((f) => {
9788
9979
  sections.push(`- ${f}`);
9789
9980
  });
9790
- sections.push(`- **Memory:** When you discover important domain knowledge, architectural decisions, or infrastructure details, store them via \`harmony_remember\`. Focus on durable knowledge that future agents would benefit from not ephemeral task details (those are auto-extracted from your session).`);
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)`);
9791
9986
  sections.push(`
9792
9987
  ## Suggested Outputs`);
9793
9988
  roleFraming.outputSuggestions.forEach((s) => {
@@ -26383,13 +26578,13 @@ function levenshteinSimilarity(a, b) {
26383
26578
  const maxLen = Math.max(sa.length, sb.length);
26384
26579
  return 1 - matrix[sa.length][sb.length] / maxLen;
26385
26580
  }
26386
- async function extractMidSessionLearnings(client2, ctx) {
26581
+ async function extractMidSessionLearnings(_client, ctx) {
26387
26582
  const workspaceId = getActiveWorkspaceId();
26388
26583
  if (!workspaceId)
26389
26584
  return { count: 0, entityIds: [] };
26390
- const projectId = getActiveProjectId() || undefined;
26585
+ const _projectId = getActiveProjectId() || undefined;
26391
26586
  const now = Date.now();
26392
- const entityIds = [];
26587
+ const _entityIds = [];
26393
26588
  const history = sessionTaskHistory.get(ctx.cardId);
26394
26589
  if (ctx.currentTask) {
26395
26590
  const previousTask = history?.lastTask || "";
@@ -26422,81 +26617,22 @@ async function extractMidSessionLearnings(client2, ctx) {
26422
26617
  }
26423
26618
  }
26424
26619
  if (ctx.status === "blocked" && ctx.blockers?.length) {
26425
- for (const blocker of ctx.blockers) {
26426
- try {
26427
- const result = await client2.createMemoryEntity({
26428
- workspace_id: workspaceId,
26429
- project_id: projectId,
26430
- type: "error",
26431
- scope: "project",
26432
- memory_tier: "draft",
26433
- title: `Blocker (mid-session): ${blocker.slice(0, 100)}`,
26434
- content: `Encountered while working on "${ctx.cardTitle}":
26435
-
26436
- ${blocker}
26437
-
26438
- Agent: ${ctx.agentName}
26439
- Progress: ${ctx.progressPercent ?? "unknown"}%`,
26440
- confidence: 0.5,
26441
- tags: ["auto-extracted", "blocker", "mid-session"],
26442
- metadata: {
26443
- source: "mid_session",
26444
- card_id: ctx.cardId
26445
- },
26446
- agent_identifier: ctx.agentIdentifier
26447
- });
26448
- const entity = result.entity;
26449
- if (entity?.id)
26450
- entityIds.push(entity.id);
26451
- } catch {}
26452
- }
26453
26620
  sessionTaskHistory.set(ctx.cardId, {
26454
26621
  lastTask: ctx.currentTask || "",
26455
26622
  lastExtractionAt: now,
26456
26623
  steps: history?.steps || []
26457
26624
  });
26458
- return { count: entityIds.length, entityIds };
26625
+ return { count: 0, entityIds: [] };
26459
26626
  }
26460
26627
  if (ctx.currentTask) {
26461
- const previousTask = history?.lastTask || "";
26462
- const similarity = levenshteinSimilarity(previousTask, ctx.currentTask);
26463
- if (similarity < 0.6 && previousTask.length > 0) {
26464
- try {
26465
- const result = await client2.createMemoryEntity({
26466
- workspace_id: workspaceId,
26467
- project_id: projectId,
26468
- type: "context",
26469
- scope: "project",
26470
- memory_tier: "draft",
26471
- title: `Task transition: ${ctx.cardTitle}`,
26472
- content: `Agent transitioned tasks on "${ctx.cardTitle}".
26473
-
26474
- Previous: ${previousTask}
26475
- Current: ${ctx.currentTask}
26476
- Progress: ${ctx.progressPercent ?? "unknown"}%`,
26477
- confidence: 0.5,
26478
- tags: ["auto-extracted", "task-transition", "mid-session"],
26479
- metadata: {
26480
- source: "mid_session",
26481
- card_id: ctx.cardId,
26482
- previous_task: previousTask,
26483
- current_task: ctx.currentTask
26484
- },
26485
- agent_identifier: ctx.agentIdentifier
26486
- });
26487
- const entity = result.entity;
26488
- if (entity?.id)
26489
- entityIds.push(entity.id);
26490
- } catch {}
26491
- }
26492
26628
  const currentHistory = sessionTaskHistory.get(ctx.cardId);
26493
26629
  sessionTaskHistory.set(ctx.cardId, {
26494
26630
  lastTask: ctx.currentTask,
26495
- lastExtractionAt: entityIds.length > 0 ? now : currentHistory?.lastExtractionAt ?? 0,
26631
+ lastExtractionAt: currentHistory?.lastExtractionAt ?? 0,
26496
26632
  steps: currentHistory?.steps || []
26497
26633
  });
26498
26634
  }
26499
- return { count: entityIds.length, entityIds };
26635
+ return { count: 0, entityIds: [] };
26500
26636
  }
26501
26637
  function clearMidSessionTracking(cardId) {
26502
26638
  sessionTaskHistory.delete(cardId);
@@ -26680,49 +26816,60 @@ async function extractLearnings(client2, session) {
26680
26816
  Related: ${relatedEntityTitles.map((t) => `[[${t}]]`).join(", ")}` : "";
26681
26817
  if (session.blockers && session.blockers.length > 0) {
26682
26818
  for (const blocker of session.blockers) {
26683
- learnings.push({
26684
- title: `Blocker: ${blocker.slice(0, 100)}`,
26685
- content: `Encountered while working on "${session.cardTitle}":
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}":
26686
26830
 
26687
26831
  ${blocker}
26688
26832
 
26689
26833
  Agent: ${session.agentName}
26690
26834
  Session status: ${session.status}`,
26691
- type: "error",
26692
- tier: "reference",
26693
- confidence: 0.7,
26694
- tags: ["auto-extracted", "blocker", ...session.cardLabels.slice(0, 3)],
26695
- metadata: {
26696
- source: "active_learning",
26697
- card_id: session.cardId
26698
- }
26699
- });
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
+ }
26700
26849
  }
26701
26850
  }
26702
- const hasMeaningfulContent = (session.blockers?.length ?? 0) > 0 || session.status === "paused" || (session.cardSubtasks?.length ?? 0) > 0 && session.cardSubtasks?.some((s) => !s.done);
26703
- if (session.status === "completed" && hasMeaningfulContent) {
26851
+ if (session.status === "paused" && (session.blockers?.length ?? 0) > 0) {
26704
26852
  const durationInfo = session.sessionDurationMs ? `
26705
26853
  Duration: ${Math.round(session.sessionDurationMs / 60000)} minutes` : "";
26706
26854
  learnings.push({
26707
- title: `Session: ${session.cardTitle}`,
26855
+ title: `Paused: ${session.cardTitle}`,
26708
26856
  content: [
26709
- `Completed work on "${session.cardTitle}".`,
26710
- session.currentTask ? `Final task: ${session.currentTask}` : "",
26857
+ `Paused work on "${session.cardTitle}".`,
26858
+ session.currentTask ? `Last task: ${session.currentTask}` : "",
26711
26859
  session.progressPercent !== undefined ? `Progress: ${session.progressPercent}%` : "",
26712
26860
  durationInfo,
26713
- session.cardLabels.length > 0 ? `Labels: ${session.cardLabels.join(", ")}` : "",
26714
- session.blockers?.length ? `Blockers encountered: ${session.blockers.join("; ")}` : "",
26861
+ session.blockers?.length ? `Blockers: ${session.blockers.join("; ")}` : "",
26715
26862
  `
26716
26863
  Agent: ${session.agentName}`,
26717
26864
  wikiLinksLine
26718
26865
  ].filter(Boolean).join(`
26719
26866
  `),
26720
26867
  type: "lesson",
26721
- tier: "episode",
26722
- confidence: 0.7,
26868
+ tier: "draft",
26869
+ confidence: 0.6,
26723
26870
  tags: [
26724
26871
  "auto-extracted",
26725
- "session-summary",
26872
+ "session-paused",
26726
26873
  ...session.cardLabels.slice(0, 3)
26727
26874
  ],
26728
26875
  metadata: {
@@ -26731,35 +26878,14 @@ Agent: ${session.agentName}`,
26731
26878
  }
26732
26879
  });
26733
26880
  }
26734
- const hasBugLabel = session.cardLabels.some((l) => ["bug", "fix", "hotfix", "defect", "error"].includes(l.toLowerCase()));
26735
- if (hasBugLabel && session.status === "completed") {
26736
- learnings.push({
26737
- title: `Solution: ${session.cardTitle}`,
26738
- content: [
26739
- `Resolved bug: "${session.cardTitle}"`,
26740
- session.currentTask ? `
26741
- Approach: ${session.currentTask}` : "",
26742
- `
26743
- Agent: ${session.agentName}`,
26744
- wikiLinksLine
26745
- ].filter(Boolean).join(`
26746
- `),
26747
- type: "solution",
26748
- tier: "reference",
26749
- confidence: 0.8,
26750
- tags: ["auto-extracted", "bug-fix", ...session.cardLabels.slice(0, 3)],
26751
- metadata: {
26752
- source: "active_learning",
26753
- card_id: session.cardId,
26754
- auto_confidence: true
26755
- }
26756
- });
26757
- }
26758
26881
  const entityIds = [];
26759
26882
  const stepHistory = sessionTaskHistory.get(session.cardId);
26760
- const hasEnoughSteps = stepHistory && stepHistory.steps.length >= 2;
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;
26761
26887
  const isSuccessful = session.status === "completed" && (session.progressPercent === undefined || session.progressPercent >= 85) && !session.blockers?.length;
26762
- if (isSuccessful && hasEnoughSteps) {
26888
+ if (isSuccessful && hasEnoughSteps && hasMinDuration) {
26763
26889
  const procedureResult = await extractOrReinforceProcedure(client2, session, stepHistory.steps, workspaceId, projectId, wikiLinksLine);
26764
26890
  if (procedureResult) {
26765
26891
  if (procedureResult.mode === "created") {
@@ -26803,218 +26929,9 @@ Agent: ${session.agentName}`,
26803
26929
  if (createdPairs.length >= 2) {
26804
26930
  linkSessionEntities(client2, createdPairs, workspaceId, projectId).catch(() => {});
26805
26931
  }
26806
- if (entityIds.length > 0) {
26807
- detectAndCreatePatterns(client2, entityIds, session, workspaceId, projectId).catch(() => {});
26808
- }
26809
- if (createdPairs.length > 0) {
26810
- detectCausalPatterns(client2, createdPairs, session, workspaceId, projectId).catch(() => {});
26811
- }
26812
26932
  clearMidSessionTracking(session.cardId);
26813
26933
  return { count: entityIds.length, entityIds };
26814
26934
  }
26815
- var PATTERN_THRESHOLD = 3;
26816
- async function detectAndCreatePatterns(client2, newEntityIds, session, workspaceId, projectId) {
26817
- const patternEntityIds = [];
26818
- for (const newEntityId of newEntityIds) {
26819
- try {
26820
- const { entity: rawEntity } = await client2.getMemoryEntity(newEntityId);
26821
- const entity = rawEntity;
26822
- if (!entity?.type)
26823
- continue;
26824
- const similar = await findSimilarEntities(client2, entity.title, entity.content, workspaceId, { projectId, limit: 30, minRrfScore: 0.01 });
26825
- const existing = similar.filter((c) => !newEntityIds.includes(c.id) && c.type === entity.type);
26826
- if (existing.length < PATTERN_THRESHOLD)
26827
- continue;
26828
- const memberTitles = [
26829
- entity.title,
26830
- ...existing.slice(0, 4).map((e) => e.title)
26831
- ];
26832
- const patternTitle = `Pattern: recurring ${entity.type} (${existing.length + 1} instances)`;
26833
- const { entities: existingPatterns } = await client2.listMemoryEntities({
26834
- workspace_id: workspaceId,
26835
- project_id: projectId,
26836
- type: "pattern",
26837
- limit: 10
26838
- });
26839
- const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_type === entity.type);
26840
- let patternId = null;
26841
- if (matchingPattern) {
26842
- patternId = matchingPattern.id;
26843
- await client2.updateMemoryEntity(patternId, {
26844
- content: `Recurring pattern: ${entity.type} entities appearing ${existing.length + 1} times.
26845
-
26846
- Members:
26847
- ${memberTitles.map((t) => `- ${t}`).join(`
26848
- `)}
26849
-
26850
- Last updated: ${new Date().toISOString()}`,
26851
- metadata: {
26852
- pattern_count: existing.length + 1,
26853
- pattern_type: entity.type,
26854
- last_updated: new Date().toISOString()
26855
- }
26856
- });
26857
- } else {
26858
- const result = await client2.createMemoryEntity({
26859
- workspace_id: workspaceId,
26860
- project_id: projectId,
26861
- type: "pattern",
26862
- scope: "project",
26863
- memory_tier: "reference",
26864
- title: patternTitle,
26865
- content: `Recurring pattern: ${entity.type} entities detected ${existing.length + 1} times.
26866
-
26867
- Members:
26868
- ${memberTitles.map((t) => `- ${t}`).join(`
26869
- `)}`,
26870
- confidence: 0.75,
26871
- tags: ["auto-extracted", "pattern", entity.type],
26872
- metadata: {
26873
- source: "pattern_detection",
26874
- pattern_type: entity.type,
26875
- pattern_count: existing.length + 1
26876
- },
26877
- agent_identifier: session.agentIdentifier
26878
- });
26879
- const created = result.entity;
26880
- if (created?.id) {
26881
- patternId = created.id;
26882
- patternEntityIds.push(patternId);
26883
- }
26884
- }
26885
- if (!patternId)
26886
- continue;
26887
- const toLink = [newEntityId, ...existing.slice(0, 4).map((e) => e.id)];
26888
- for (const sourceId of toLink) {
26889
- try {
26890
- await client2.createMemoryRelation({
26891
- source_id: sourceId,
26892
- target_id: patternId,
26893
- relation_type: "part_of",
26894
- confidence: 0.75
26895
- });
26896
- } catch {}
26897
- }
26898
- } catch {}
26899
- }
26900
- return patternEntityIds;
26901
- }
26902
- var CAUSAL_PATTERN_THRESHOLD = 3;
26903
- async function detectCausalPatterns(client2, createdPairs, session, workspaceId, projectId) {
26904
- const patternIds = [];
26905
- const errors3 = createdPairs.filter((p) => p.learning.type === "error");
26906
- const solutions = createdPairs.filter((p) => p.learning.type === "solution");
26907
- if (errors3.length === 0 || solutions.length === 0)
26908
- return patternIds;
26909
- for (const errorPair of errors3) {
26910
- try {
26911
- const similarErrors = await findSimilarEntities(client2, errorPair.learning.title, errorPair.learning.content, workspaceId, {
26912
- projectId,
26913
- limit: 20,
26914
- minRrfScore: 0.03,
26915
- excludeIds: createdPairs.map((p) => p.id),
26916
- type: "error"
26917
- });
26918
- const resolvedErrors = [];
26919
- for (const similar of similarErrors.slice(0, 10)) {
26920
- try {
26921
- const { outgoing } = await client2.getRelatedEntities(similar.id);
26922
- const resolvedByRel = outgoing.find((r) => r.relation_type === "resolved_by");
26923
- if (resolvedByRel) {
26924
- resolvedErrors.push({
26925
- errorId: similar.id,
26926
- errorTitle: similar.title,
26927
- solutionTitle: resolvedByRel.target_title || "unknown"
26928
- });
26929
- }
26930
- } catch {}
26931
- }
26932
- if (resolvedErrors.length + 1 < CAUSAL_PATTERN_THRESHOLD)
26933
- continue;
26934
- const { entities: existingPatterns } = await client2.listMemoryEntities({
26935
- workspace_id: workspaceId,
26936
- project_id: projectId,
26937
- type: "pattern",
26938
- limit: 10
26939
- });
26940
- const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_chain_type === "error_resolved_by_solution");
26941
- if (matchingPattern) {
26942
- await client2.updateMemoryEntity(matchingPattern.id, {
26943
- content: [
26944
- `Recurring error→solution chain detected (${resolvedErrors.length + 1} instances).`,
26945
- "",
26946
- "## Error→Solution Pairs",
26947
- `- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
26948
- ...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`),
26949
- "",
26950
- `Last updated: ${new Date().toISOString()}`
26951
- ].join(`
26952
- `),
26953
- metadata: {
26954
- pattern_chain_type: "error_resolved_by_solution",
26955
- pattern_count: resolvedErrors.length + 1,
26956
- last_updated: new Date().toISOString()
26957
- }
26958
- });
26959
- for (const pair of [errorPair, solutions[0]]) {
26960
- try {
26961
- await client2.createMemoryRelation({
26962
- source_id: pair.id,
26963
- target_id: matchingPattern.id,
26964
- relation_type: "part_of",
26965
- confidence: 0.75
26966
- });
26967
- } catch {}
26968
- }
26969
- } else {
26970
- const result = await client2.createMemoryEntity({
26971
- workspace_id: workspaceId,
26972
- project_id: projectId,
26973
- type: "pattern",
26974
- scope: "project",
26975
- memory_tier: "reference",
26976
- title: `Pattern: recurring error→solution chain (${resolvedErrors.length + 1} instances)`,
26977
- content: [
26978
- `Recurring error→solution chain detected across ${resolvedErrors.length + 1} sessions.`,
26979
- "",
26980
- "## Error→Solution Pairs",
26981
- `- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
26982
- ...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`)
26983
- ].join(`
26984
- `),
26985
- confidence: 0.8,
26986
- tags: ["auto-extracted", "pattern", "causal-chain"],
26987
- metadata: {
26988
- source: "causal_pattern_detection",
26989
- pattern_chain_type: "error_resolved_by_solution",
26990
- pattern_count: resolvedErrors.length + 1
26991
- },
26992
- agent_identifier: session.agentIdentifier
26993
- });
26994
- const created = result.entity;
26995
- if (created?.id) {
26996
- patternIds.push(created.id);
26997
- const memberIds = [
26998
- errorPair.id,
26999
- solutions[0].id,
27000
- ...resolvedErrors.slice(0, 4).map((r) => r.errorId)
27001
- ];
27002
- for (const memberId of memberIds) {
27003
- try {
27004
- await client2.createMemoryRelation({
27005
- source_id: memberId,
27006
- target_id: created.id,
27007
- relation_type: "part_of",
27008
- confidence: 0.75
27009
- });
27010
- } catch {}
27011
- }
27012
- }
27013
- }
27014
- } catch {}
27015
- }
27016
- return patternIds;
27017
- }
27018
26935
  async function detectContradictions(client2, entityId, entityType, title, content, tags, workspaceId, projectId) {
27019
26936
  if (!CONTRADICTION_TYPES.has(entityType))
27020
26937
  return [];
@@ -27827,7 +27744,7 @@ async function autoEndSession(client3, cardId, status) {
27827
27744
  // src/consolidation.ts
27828
27745
  async function consolidateMemories(client3, workspaceId, projectId, options) {
27829
27746
  const dryRun = options?.dryRun !== false;
27830
- const minClusterSize = options?.minClusterSize ?? 2;
27747
+ const minClusterSize = options?.minClusterSize ?? 3;
27831
27748
  const result = {
27832
27749
  consolidated: 0,
27833
27750
  clustersFound: 0,
@@ -27892,12 +27809,7 @@ async function consolidateMemories(client3, workspaceId, projectId, options) {
27892
27809
  result.clustersFound++;
27893
27810
  const mergedTitle = deriveClusterTitle(cluster, type);
27894
27811
  const memberTitles = cluster.map((e) => e.title);
27895
- const mergedContent = [
27896
- `Consolidated from ${cluster.length} ${type} memories:
27897
- `,
27898
- ...cluster.map((e) => `- **${e.title}**: ${e.content.slice(0, 200)}`)
27899
- ].join(`
27900
- `);
27812
+ const mergedContent = synthesizeClusterContent(cluster, type);
27901
27813
  const maxConfidence = Math.max(...cluster.map((e) => e.confidence));
27902
27814
  const allTags = [...new Set(cluster.flatMap((e) => e.tags || []))];
27903
27815
  const detail = {
@@ -27959,6 +27871,60 @@ async function consolidateMemories(client3, workspaceId, projectId, options) {
27959
27871
  }
27960
27872
  return result;
27961
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
+ }
27962
27928
  function deriveClusterTitle(cluster, type) {
27963
27929
  const stopWords = new Set([
27964
27930
  "the",
@@ -28013,9 +27979,9 @@ function deriveClusterTitle(cluster, type) {
28013
27979
  wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
28014
27980
  }
28015
27981
  }
28016
- const topWords = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([word]) => word);
28017
- const suffix = topWords.length > 0 ? topWords.join(", ") : "various";
28018
- return `Consolidated ${type}: ${suffix}`;
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}`;
28019
27985
  }
28020
27986
 
28021
27987
  // src/server.ts
@@ -28091,6 +28057,371 @@ async function runLifecycleMaintenance(client3, workspaceId, projectId) {
28091
28057
  return result;
28092
28058
  }
28093
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
+
28094
28425
  // src/onboard.ts
28095
28426
  async function onboardNewUser(params) {
28096
28427
  const {
@@ -29525,6 +29856,47 @@ var TOOLS = {
29525
29856
  },
29526
29857
  required: []
29527
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
+ }
29528
29900
  }
29529
29901
  };
29530
29902
  var RESOURCES = [
@@ -31035,6 +31407,27 @@ async function handleToolCall(name, args, deps) {
31035
31407
  message: `Processed ${entitiesProcessed} entities, created ${relationsCreated} new relations.`
31036
31408
  };
31037
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
+ }
31038
31431
  default:
31039
31432
  throw new Error(`Unknown tool: ${name}`);
31040
31433
  }
@@ -31103,7 +31496,7 @@ class HarmonyMCPServer {
31103
31496
  // src/skills.ts
31104
31497
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
31105
31498
  import { dirname } from "node:path";
31106
- var SKILLS_VERSION = "3";
31499
+ var SKILLS_VERSION = "4";
31107
31500
  var VERSION_MARKER_PREFIX = "<!-- skills-version:";
31108
31501
  var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
31109
31502
 
@@ -31297,6 +31690,30 @@ Do not start an agent session.
31297
31690
 
31298
31691
  ---
31299
31692
 
31693
+ ## Step E: Auto-Detect Card for Implementation Tasks
31694
+
31695
+ **IMPORTANT:** This step applies when you are about to implement a plan, feature, or fix
31696
+ that was NOT started via /hmy. Before writing any code, check if the work maps to an
31697
+ existing Harmony card.
31698
+
31699
+ ### When to run this check
31700
+ - User says "implement this plan", "build this feature", "fix this bug" (without /hmy)
31701
+ - You are about to execute a plan file
31702
+ - Skip if: the user already started with /hmy, or no Harmony MCP tools are available
31703
+
31704
+ ### Detection steps
31705
+ 1. Call \`harmony_search_cards\` with keywords from the plan title or task description
31706
+ 2. If a card matches (same feature/fix described), tell the user:
31707
+ "This maps to card #N — starting a session to track progress."
31708
+ 3. Call \`harmony_start_agent_session\` with \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
31709
+ 4. Track progress with \`harmony_update_agent_progress\` at milestones
31710
+ 5. When done, call \`harmony_end_agent_session\` with \`status: "completed"\`, \`moveToColumn: "Review"\`
31711
+
31712
+ ### If no match
31713
+ Proceed normally without a session. No action needed.
31714
+
31715
+ ---
31716
+
31300
31717
  ## Key Tools Reference
31301
31718
 
31302
31719
  **Cards:** \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\`, \`harmony_create_card\`, \`harmony_update_card\`, \`harmony_move_card\`, \`harmony_delete_card\`, \`harmony_assign_card\`
@@ -33477,6 +33894,17 @@ When given a card reference (e.g., #42 or a card name), follow this workflow:
33477
33894
  7. Update progress periodically with \`harmony_update_agent_progress\`
33478
33895
  8. When done, call \`harmony_end_agent_session\` and move to "Review"
33479
33896
 
33897
+ ## Auto-Detect Card for Implementation Tasks
33898
+
33899
+ Before implementing a plan or feature, check if it maps to an existing Harmony card:
33900
+
33901
+ 1. Use \`harmony_search_cards\` with keywords from the task description
33902
+ 2. If a match is found, call \`harmony_start_agent_session\` (agentIdentifier: "claude-code", agentName: "Claude Code", moveToColumn: "In Progress", addLabels: ["agent"])
33903
+ 3. Update progress with \`harmony_update_agent_progress\` at milestones
33904
+ 4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
33905
+
33906
+ Skip if: work was already started with a card reference, or no matching card exists.
33907
+
33480
33908
  ## Available Harmony Tools
33481
33909
 
33482
33910
  - \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\` - Find cards