@gethmy/mcp 2.2.4 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 scored = candidates.map((entity) => {
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.1, TIER_WEIGHTS, PROCEDURE_BUDGET_FRACTION = 0.15, TIER_BUDGET_ALLOCATION, MIN_REFERENCE_SLOTS = 3, 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;
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:** 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).`);
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(client2, ctx) {
24341
+ async function extractMidSessionLearnings(_client, ctx) {
24295
24342
  const workspaceId = getActiveWorkspaceId();
24296
24343
  if (!workspaceId)
24297
24344
  return { count: 0, entityIds: [] };
24298
- const projectId = getActiveProjectId() || undefined;
24345
+ const _projectId = getActiveProjectId() || undefined;
24299
24346
  const now = Date.now();
24300
- const entityIds = [];
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: entityIds.length, entityIds };
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: entityIds.length > 0 ? now : currentHistory?.lastExtractionAt ?? 0,
24391
+ lastExtractionAt: currentHistory?.lastExtractionAt ?? 0,
24404
24392
  steps: currentHistory?.steps || []
24405
24393
  });
24406
24394
  }
24407
- return { count: entityIds.length, entityIds };
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
- learnings.push({
24592
- title: `Blocker: ${blocker.slice(0, 100)}`,
24593
- content: `Encountered while working on "${session.cardTitle}":
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
- type: "error",
24600
- tier: "reference",
24601
- confidence: 0.7,
24602
- tags: ["auto-extracted", "blocker", ...session.cardLabels.slice(0, 3)],
24603
- metadata: {
24604
- source: "active_learning",
24605
- card_id: session.cardId
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
- const hasMeaningfulContent = (session.blockers?.length ?? 0) > 0 || session.status === "paused" || (session.cardSubtasks?.length ?? 0) > 0 && session.cardSubtasks?.some((s) => !s.done);
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: `Session: ${session.cardTitle}`,
24615
+ title: `Paused: ${session.cardTitle}`,
24616
24616
  content: [
24617
- `Completed work on "${session.cardTitle}".`,
24618
- session.currentTask ? `Final task: ${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.cardLabels.length > 0 ? `Labels: ${session.cardLabels.join(", ")}` : "",
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: "episode",
24630
- confidence: 0.7,
24628
+ tier: "draft",
24629
+ confidence: 0.6,
24631
24630
  tags: [
24632
24631
  "auto-extracted",
24633
- "session-summary",
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 hasEnoughSteps = stepHistory && stepHistory.steps.length >= 2;
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 [];
@@ -25264,6 +25033,12 @@ class HarmonyApiClient {
25264
25033
  async endAgentSession(cardId, data) {
25265
25034
  return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
25266
25035
  }
25036
+ async flushActivityLog(cardId, data) {
25037
+ return this.request("POST", `/cards/${cardId}/agent-activity-log`, data);
25038
+ }
25039
+ async getActivityLog(cardId, sessionId) {
25040
+ return this.request("GET", `/cards/${cardId}/agent-activity-log?sessionId=${sessionId}`);
25041
+ }
25267
25042
  async getAgentSession(cardId, options) {
25268
25043
  const params = new URLSearchParams;
25269
25044
  if (options?.includeEnded)
@@ -25735,7 +25510,7 @@ async function autoEndSession(client3, cardId, status) {
25735
25510
  // src/consolidation.ts
25736
25511
  async function consolidateMemories(client3, workspaceId, projectId, options) {
25737
25512
  const dryRun = options?.dryRun !== false;
25738
- const minClusterSize = options?.minClusterSize ?? 2;
25513
+ const minClusterSize = options?.minClusterSize ?? 3;
25739
25514
  const result = {
25740
25515
  consolidated: 0,
25741
25516
  clustersFound: 0,
@@ -25800,12 +25575,7 @@ async function consolidateMemories(client3, workspaceId, projectId, options) {
25800
25575
  result.clustersFound++;
25801
25576
  const mergedTitle = deriveClusterTitle(cluster, type);
25802
25577
  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
- `);
25578
+ const mergedContent = synthesizeClusterContent(cluster, type);
25809
25579
  const maxConfidence = Math.max(...cluster.map((e) => e.confidence));
25810
25580
  const allTags = [...new Set(cluster.flatMap((e) => e.tags || []))];
25811
25581
  const detail = {
@@ -25867,6 +25637,60 @@ async function consolidateMemories(client3, workspaceId, projectId, options) {
25867
25637
  }
25868
25638
  return result;
25869
25639
  }
25640
+ function synthesizeClusterContent(cluster, type) {
25641
+ const SKIP_PATTERNS = [
25642
+ /^##\s/,
25643
+ /^Agent:/,
25644
+ /^Duration:/,
25645
+ /^Labels:/,
25646
+ /^Progress:/,
25647
+ /^Session status:/,
25648
+ /^Completed at/,
25649
+ /^Final state:/,
25650
+ /^Related:/,
25651
+ /^When working on:/,
25652
+ /^\d+\.\s+.+\(\d+%,\s*\+\d+%\)/,
25653
+ /^Last updated:/,
25654
+ /^Recurring pattern:/,
25655
+ /^Consolidated from/
25656
+ ];
25657
+ const seenLines = new Set;
25658
+ const knowledgeLines = [];
25659
+ for (const entity of cluster) {
25660
+ const lines = entity.content.split(`
25661
+ `).map((l) => l.trim());
25662
+ for (const line of lines) {
25663
+ if (!line || line.length < 20)
25664
+ continue;
25665
+ if (SKIP_PATTERNS.some((p) => p.test(line)))
25666
+ continue;
25667
+ const normalized = line.toLowerCase().replace(/[*_`#[\]]/g, "").trim();
25668
+ if (seenLines.has(normalized))
25669
+ continue;
25670
+ seenLines.add(normalized);
25671
+ knowledgeLines.push(line);
25672
+ }
25673
+ }
25674
+ if (knowledgeLines.length === 0) {
25675
+ return `${cluster.length} related ${type} entities consolidated. Original titles:
25676
+ ${cluster.map((e) => `- ${e.title}`).join(`
25677
+ `)}`;
25678
+ }
25679
+ const MAX_CHARS = 1600;
25680
+ const result = [
25681
+ `Consolidated knowledge from ${cluster.length} ${type} entities:
25682
+ `
25683
+ ];
25684
+ let charCount = result[0].length;
25685
+ for (const line of knowledgeLines) {
25686
+ if (charCount + line.length + 3 > MAX_CHARS)
25687
+ break;
25688
+ result.push(`- ${line}`);
25689
+ charCount += line.length + 3;
25690
+ }
25691
+ return result.join(`
25692
+ `);
25693
+ }
25870
25694
  function deriveClusterTitle(cluster, type) {
25871
25695
  const stopWords = new Set([
25872
25696
  "the",
@@ -25921,9 +25745,9 @@ function deriveClusterTitle(cluster, type) {
25921
25745
  wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
25922
25746
  }
25923
25747
  }
25924
- const topWords = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([word]) => word);
25925
- const suffix = topWords.length > 0 ? topWords.join(", ") : "various";
25926
- return `Consolidated ${type}: ${suffix}`;
25748
+ const topWords = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 4).map(([word]) => word[0].toUpperCase() + word.slice(1));
25749
+ const suffix = topWords.length > 0 ? topWords.join(" / ") : "Various";
25750
+ return `${type[0].toUpperCase() + type.slice(1)}: ${suffix}`;
25927
25751
  }
25928
25752
 
25929
25753
  // src/server.ts
@@ -25999,6 +25823,409 @@ async function runLifecycleMaintenance(client3, workspaceId, projectId) {
25999
25823
  return result;
26000
25824
  }
26001
25825
 
25826
+ // src/memory-cleanup.ts
25827
+ init_dist();
25828
+ var ALL_STEPS = [
25829
+ "prune",
25830
+ "consolidate",
25831
+ "orphans",
25832
+ "duplicates",
25833
+ "backfill"
25834
+ ];
25835
+ var MS_PER_DAY = 1000 * 60 * 60 * 24;
25836
+ var MAX_ENTITIES_FETCH = 200;
25837
+ var DUPLICATE_SIMILARITY_THRESHOLD = 0.85;
25838
+ var CONCURRENCY_LIMIT = 5;
25839
+ async function runMemoryCleanup(client3, workspaceId, projectId, options) {
25840
+ const dryRun = options?.dryRun !== false;
25841
+ const steps = options?.steps ?? ALL_STEPS;
25842
+ const maxAgeDays = options?.maxAgeDays ?? 30;
25843
+ const minClusterSize = options?.minClusterSize ?? 3;
25844
+ const orphanAgeDays = options?.orphanAgeDays ?? 14;
25845
+ const report = {
25846
+ success: true,
25847
+ dryRun,
25848
+ timestamp: new Date().toISOString(),
25849
+ workspace: { id: workspaceId, projectId },
25850
+ summary: { totalEntities: 0, issuesFound: 0, actionsTaken: 0 },
25851
+ steps: {},
25852
+ errors: [],
25853
+ healthReport: ""
25854
+ };
25855
+ let entities = [];
25856
+ try {
25857
+ const listResult = await client3.listMemoryEntities({
25858
+ workspace_id: workspaceId,
25859
+ project_id: projectId,
25860
+ limit: MAX_ENTITIES_FETCH
25861
+ });
25862
+ entities = listResult.entities || [];
25863
+ report.summary.totalEntities = entities.length;
25864
+ } catch (err) {
25865
+ report.errors.push({
25866
+ step: "init",
25867
+ message: `Failed to fetch entities: ${err.message}`
25868
+ });
25869
+ report.success = false;
25870
+ report.healthReport = generateHealthReport(report);
25871
+ return report;
25872
+ }
25873
+ if (steps.includes("prune")) {
25874
+ try {
25875
+ report.steps.prune = runPruneStep(entities, maxAgeDays);
25876
+ if (!dryRun) {
25877
+ for (const item of report.steps.prune.items) {
25878
+ try {
25879
+ await client3.deleteMemoryEntity(item.id);
25880
+ report.steps.prune.pruned++;
25881
+ } catch (err) {
25882
+ report.errors.push({
25883
+ step: "prune",
25884
+ message: `Failed to delete ${item.id}: ${err.message}`
25885
+ });
25886
+ }
25887
+ }
25888
+ report.summary.actionsTaken += report.steps.prune.pruned;
25889
+ }
25890
+ report.summary.issuesFound += report.steps.prune.staleDraftsFound;
25891
+ } catch (err) {
25892
+ report.errors.push({
25893
+ step: "prune",
25894
+ message: err.message
25895
+ });
25896
+ }
25897
+ }
25898
+ if (steps.includes("consolidate")) {
25899
+ try {
25900
+ const result = await consolidateMemories(client3, workspaceId, projectId, {
25901
+ dryRun,
25902
+ minClusterSize
25903
+ });
25904
+ report.steps.consolidate = {
25905
+ clustersFound: result.clustersFound,
25906
+ entitiesProcessed: result.entitiesProcessed,
25907
+ consolidated: result.consolidated,
25908
+ details: result.details
25909
+ };
25910
+ report.summary.issuesFound += result.clustersFound;
25911
+ if (!dryRun)
25912
+ report.summary.actionsTaken += result.consolidated;
25913
+ } catch (err) {
25914
+ report.errors.push({
25915
+ step: "consolidate",
25916
+ message: err.message
25917
+ });
25918
+ }
25919
+ }
25920
+ if (steps.includes("orphans")) {
25921
+ try {
25922
+ report.steps.orphans = await runOrphanStep(client3, entities, orphanAgeDays);
25923
+ if (!dryRun) {
25924
+ for (const item of report.steps.orphans.items) {
25925
+ try {
25926
+ await client3.deleteMemoryEntity(item.id);
25927
+ report.steps.orphans.removed++;
25928
+ } catch (err) {
25929
+ report.errors.push({
25930
+ step: "orphans",
25931
+ message: `Failed to delete ${item.id}: ${err.message}`
25932
+ });
25933
+ }
25934
+ }
25935
+ report.summary.actionsTaken += report.steps.orphans.removed;
25936
+ }
25937
+ report.summary.issuesFound += report.steps.orphans.orphansFound;
25938
+ } catch (err) {
25939
+ report.errors.push({
25940
+ step: "orphans",
25941
+ message: err.message
25942
+ });
25943
+ }
25944
+ }
25945
+ if (steps.includes("duplicates")) {
25946
+ try {
25947
+ report.steps.duplicates = await runDuplicateStep(client3, entities, workspaceId, projectId);
25948
+ if (!dryRun) {
25949
+ for (const pair of report.steps.duplicates.pairs) {
25950
+ try {
25951
+ await client3.deleteMemoryEntity(pair.removeId);
25952
+ report.steps.duplicates.resolved++;
25953
+ } catch (err) {
25954
+ report.errors.push({
25955
+ step: "duplicates",
25956
+ message: `Failed to delete ${pair.removeId}: ${err.message}`
25957
+ });
25958
+ }
25959
+ }
25960
+ report.summary.actionsTaken += report.steps.duplicates.resolved;
25961
+ }
25962
+ report.summary.issuesFound += report.steps.duplicates.duplicatePairsFound;
25963
+ } catch (err) {
25964
+ report.errors.push({
25965
+ step: "duplicates",
25966
+ message: err.message
25967
+ });
25968
+ }
25969
+ }
25970
+ if (steps.includes("backfill")) {
25971
+ try {
25972
+ if (dryRun) {
25973
+ report.steps.backfill = {
25974
+ processed: 0,
25975
+ remaining: -1,
25976
+ errors: []
25977
+ };
25978
+ } else {
25979
+ const result = await client3.backfillEmbeddings(workspaceId);
25980
+ report.steps.backfill = {
25981
+ processed: result.processed,
25982
+ remaining: result.remaining,
25983
+ errors: result.errors || []
25984
+ };
25985
+ report.summary.actionsTaken += result.processed;
25986
+ }
25987
+ } catch (err) {
25988
+ report.errors.push({
25989
+ step: "backfill",
25990
+ message: err.message
25991
+ });
25992
+ }
25993
+ }
25994
+ report.healthReport = generateHealthReport(report);
25995
+ return report;
25996
+ }
25997
+ function runPruneStep(entities, maxAgeDays) {
25998
+ const now = Date.now();
25999
+ const drafts = entities.filter((e) => e.memory_tier === "draft");
26000
+ const stale = [];
26001
+ for (const entity of drafts) {
26002
+ const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY;
26003
+ if (ageDays < maxAgeDays)
26004
+ continue;
26005
+ const lifecycle2 = evaluateLifecycle(entity);
26006
+ stale.push({
26007
+ id: entity.id,
26008
+ title: entity.title,
26009
+ ageDays: Math.round(ageDays),
26010
+ decayScore: Math.round(lifecycle2.decay.score * 100) / 100
26011
+ });
26012
+ }
26013
+ return { staleDraftsFound: stale.length, pruned: 0, items: stale };
26014
+ }
26015
+ async function runOrphanStep(client3, entities, orphanAgeDays) {
26016
+ const now = Date.now();
26017
+ const result = { orphansFound: 0, removed: 0, items: [] };
26018
+ const candidates = entities.filter((e) => {
26019
+ if (e.memory_tier === "reference")
26020
+ return false;
26021
+ if (e.access_count >= 2)
26022
+ return false;
26023
+ const ageDays = (now - new Date(e.created_at).getTime()) / MS_PER_DAY;
26024
+ return ageDays >= orphanAgeDays;
26025
+ });
26026
+ for (let i = 0;i < candidates.length; i += CONCURRENCY_LIMIT) {
26027
+ const batch = candidates.slice(i, i + CONCURRENCY_LIMIT);
26028
+ const results = await Promise.allSettled(batch.map(async (entity) => {
26029
+ const related = await client3.getRelatedEntities(entity.id);
26030
+ const totalRelations = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
26031
+ if (totalRelations > 0)
26032
+ return null;
26033
+ const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY;
26034
+ return {
26035
+ id: entity.id,
26036
+ title: entity.title,
26037
+ type: entity.type,
26038
+ tier: entity.memory_tier,
26039
+ ageDays: Math.round(ageDays),
26040
+ accessCount: entity.access_count
26041
+ };
26042
+ }));
26043
+ for (const r of results) {
26044
+ if (r.status === "fulfilled" && r.value) {
26045
+ result.items.push(r.value);
26046
+ result.orphansFound++;
26047
+ }
26048
+ }
26049
+ }
26050
+ return result;
26051
+ }
26052
+ async function runDuplicateStep(client3, entities, workspaceId, projectId) {
26053
+ const result = {
26054
+ duplicatePairsFound: 0,
26055
+ resolved: 0,
26056
+ pairs: []
26057
+ };
26058
+ const seenPairs = new Set;
26059
+ const flaggedForRemoval = new Set;
26060
+ const entityMap = new Map(entities.map((e) => [e.id, e]));
26061
+ const similarityMap = new Map;
26062
+ for (let i = 0;i < entities.length; i += CONCURRENCY_LIMIT) {
26063
+ const batch = entities.slice(i, i + CONCURRENCY_LIMIT);
26064
+ const results = await Promise.allSettled(batch.map(async (entity) => {
26065
+ const similar = await findSimilarEntities(client3, entity.title, entity.content, workspaceId, { projectId, limit: 5, minRrfScore: 0.05, excludeIds: [entity.id] });
26066
+ return { entityId: entity.id, similar };
26067
+ }));
26068
+ for (const r of results) {
26069
+ if (r.status === "fulfilled") {
26070
+ similarityMap.set(r.value.entityId, r.value.similar);
26071
+ }
26072
+ }
26073
+ }
26074
+ for (const entity of entities) {
26075
+ if (flaggedForRemoval.has(entity.id))
26076
+ continue;
26077
+ const similar = similarityMap.get(entity.id) || [];
26078
+ for (const match of similar) {
26079
+ if (flaggedForRemoval.has(match.id))
26080
+ continue;
26081
+ const pairKey = [entity.id, match.id].sort().join(":");
26082
+ if (seenPairs.has(pairKey))
26083
+ continue;
26084
+ seenPairs.add(pairKey);
26085
+ const sim = titleSimilarity(entity.title, match.title);
26086
+ if (sim < DUPLICATE_SIMILARITY_THRESHOLD)
26087
+ continue;
26088
+ const entityScore = entityQualityScore(entity);
26089
+ const matchEntity = entityMap.get(match.id);
26090
+ const matchScore = matchEntity ? entityQualityScore(matchEntity) : match.confidence;
26091
+ const [keep, remove] = entityScore >= matchScore ? [entity, { id: match.id, title: match.title }] : [{ id: match.id, title: match.title }, entity];
26092
+ flaggedForRemoval.add(remove.id);
26093
+ result.pairs.push({
26094
+ keepId: keep.id,
26095
+ keepTitle: keep.title,
26096
+ removeId: remove.id,
26097
+ removeTitle: remove.title,
26098
+ similarity: Math.round(sim * 100) / 100
26099
+ });
26100
+ result.duplicatePairsFound++;
26101
+ }
26102
+ }
26103
+ return result;
26104
+ }
26105
+ var TIER_WEIGHTS2 = {
26106
+ reference: 3,
26107
+ episode: 2,
26108
+ draft: 1
26109
+ };
26110
+ function entityQualityScore(entity) {
26111
+ return entity.confidence + (TIER_WEIGHTS2[entity.memory_tier] || 0) + Math.min(entity.access_count, 10) * 0.1;
26112
+ }
26113
+ function titleSimilarity(a, b) {
26114
+ const na = a.toLowerCase().trim();
26115
+ const nb = b.toLowerCase().trim();
26116
+ if (na === nb)
26117
+ return 1;
26118
+ const wordsA = new Set(na.split(/\W+/).filter(Boolean));
26119
+ const wordsB = new Set(nb.split(/\W+/).filter(Boolean));
26120
+ if (wordsA.size === 0 || wordsB.size === 0)
26121
+ return 0;
26122
+ let intersection3 = 0;
26123
+ for (const w of wordsA) {
26124
+ if (wordsB.has(w))
26125
+ intersection3++;
26126
+ }
26127
+ const union3 = wordsA.size + wordsB.size - intersection3;
26128
+ return union3 > 0 ? intersection3 / union3 : 0;
26129
+ }
26130
+ function generateHealthReport(report) {
26131
+ const mode = report.dryRun ? "Dry Run (preview)" : "Executed";
26132
+ const lines = [
26133
+ `# Memory Health Report
26134
+ `,
26135
+ `**Mode:** ${mode} | **Entities:** ${report.summary.totalEntities} | **Issues:** ${report.summary.issuesFound} | **Actions:** ${report.summary.actionsTaken}`,
26136
+ ""
26137
+ ];
26138
+ if (report.summary.totalEntities >= MAX_ENTITIES_FETCH) {
26139
+ lines.push(`> **Note:** Entity count hit the ${MAX_ENTITIES_FETCH} fetch limit. Some entities may not have been analyzed.
26140
+ `);
26141
+ }
26142
+ if (report.steps.prune) {
26143
+ const p = report.steps.prune;
26144
+ lines.push("## Stale Drafts");
26145
+ if (p.staleDraftsFound === 0) {
26146
+ lines.push(`No stale drafts found.
26147
+ `);
26148
+ } else {
26149
+ lines.push(`Found **${p.staleDraftsFound}** stale drafts${!report.dryRun ? ` (pruned ${p.pruned})` : ""}:`);
26150
+ lines.push("| Title | Age | Decay |");
26151
+ lines.push("|-------|-----|-------|");
26152
+ for (const item of p.items.slice(0, 20)) {
26153
+ lines.push(`| ${item.title} | ${item.ageDays}d | ${item.decayScore} |`);
26154
+ }
26155
+ lines.push("");
26156
+ }
26157
+ }
26158
+ if (report.steps.consolidate) {
26159
+ const c = report.steps.consolidate;
26160
+ lines.push("## Consolidation");
26161
+ if (c.clustersFound === 0) {
26162
+ lines.push(`Scanned ${c.entitiesProcessed} draft/episode entities — no clusters found.
26163
+ `);
26164
+ } else {
26165
+ lines.push(`Found **${c.clustersFound}** clusters across ${c.entitiesProcessed} entities:`);
26166
+ for (const d of c.details.slice(0, 10)) {
26167
+ lines.push(`- **${d.mergedTitle}** — ${d.clusterSize} entities`);
26168
+ }
26169
+ lines.push("");
26170
+ }
26171
+ }
26172
+ if (report.steps.orphans) {
26173
+ const o = report.steps.orphans;
26174
+ lines.push("## Orphaned Entities");
26175
+ if (o.orphansFound === 0) {
26176
+ lines.push(`No orphans found.
26177
+ `);
26178
+ } else {
26179
+ lines.push(`Found **${o.orphansFound}** orphans${!report.dryRun ? ` (removed ${o.removed})` : ""}:`);
26180
+ lines.push("| Title | Type | Tier | Age | Accesses |");
26181
+ lines.push("|-------|------|------|-----|----------|");
26182
+ for (const item of o.items.slice(0, 20)) {
26183
+ lines.push(`| ${item.title} | ${item.type} | ${item.tier} | ${item.ageDays}d | ${item.accessCount} |`);
26184
+ }
26185
+ lines.push("");
26186
+ }
26187
+ }
26188
+ if (report.steps.duplicates) {
26189
+ const d = report.steps.duplicates;
26190
+ lines.push("## Near-Duplicates");
26191
+ if (d.duplicatePairsFound === 0) {
26192
+ lines.push(`No duplicates found.
26193
+ `);
26194
+ } else {
26195
+ lines.push(`Found **${d.duplicatePairsFound}** duplicate pairs${!report.dryRun ? ` (resolved ${d.resolved})` : ""}:`);
26196
+ for (const pair of d.pairs.slice(0, 20)) {
26197
+ lines.push(`- "${pair.keepTitle}" ~ "${pair.removeTitle}" (${Math.round(pair.similarity * 100)}% similar, keep first)`);
26198
+ }
26199
+ lines.push("");
26200
+ }
26201
+ }
26202
+ if (report.steps.backfill) {
26203
+ const b = report.steps.backfill;
26204
+ lines.push("## Embedding Coverage");
26205
+ if (report.dryRun) {
26206
+ lines.push("Backfill will run when executed with `dryRun: false`.\n");
26207
+ } else if (b.remaining === 0) {
26208
+ lines.push(`All embeddings up to date (processed ${b.processed}).
26209
+ `);
26210
+ } else {
26211
+ lines.push(`Processed ${b.processed} entities. ${b.remaining} still need embeddings.
26212
+ `);
26213
+ }
26214
+ }
26215
+ if (report.errors.length > 0) {
26216
+ lines.push("## Errors");
26217
+ for (const e of report.errors) {
26218
+ lines.push(`- **${e.step}:** ${e.message}`);
26219
+ }
26220
+ lines.push("");
26221
+ }
26222
+ if (report.dryRun) {
26223
+ lines.push("---\n*Run with `dryRun: false` to execute cleanup.*");
26224
+ }
26225
+ return lines.join(`
26226
+ `);
26227
+ }
26228
+
26002
26229
  // src/onboard.ts
26003
26230
  async function onboardNewUser(params) {
26004
26231
  const {
@@ -27433,6 +27660,47 @@ var TOOLS = {
27433
27660
  },
27434
27661
  required: []
27435
27662
  }
27663
+ },
27664
+ harmony_cleanup_memories: {
27665
+ 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.",
27666
+ inputSchema: {
27667
+ type: "object",
27668
+ properties: {
27669
+ workspaceId: {
27670
+ type: "string",
27671
+ description: "Workspace ID (optional if context set)"
27672
+ },
27673
+ projectId: {
27674
+ type: "string",
27675
+ description: "Project ID (optional)"
27676
+ },
27677
+ dryRun: {
27678
+ type: "boolean",
27679
+ description: "Preview cleanup without executing changes (default: true)"
27680
+ },
27681
+ steps: {
27682
+ type: "array",
27683
+ items: {
27684
+ type: "string",
27685
+ enum: ["prune", "consolidate", "orphans", "duplicates", "backfill"]
27686
+ },
27687
+ description: "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill."
27688
+ },
27689
+ maxAgeDays: {
27690
+ type: "number",
27691
+ description: "Max age in days for stale draft pruning (default: 30)"
27692
+ },
27693
+ minClusterSize: {
27694
+ type: "number",
27695
+ description: "Min cluster size for consolidation (default: 3)"
27696
+ },
27697
+ orphanAgeDays: {
27698
+ type: "number",
27699
+ description: "Min age in days for orphan detection (default: 14)"
27700
+ }
27701
+ },
27702
+ required: []
27703
+ }
27436
27704
  }
27437
27705
  };
27438
27706
  var RESOURCES = [
@@ -28943,6 +29211,40 @@ async function handleToolCall(name, args, deps) {
28943
29211
  message: `Processed ${entitiesProcessed} entities, created ${relationsCreated} new relations.`
28944
29212
  };
28945
29213
  }
29214
+ case "harmony_cleanup_memories": {
29215
+ const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
29216
+ if (!workspaceId) {
29217
+ throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
29218
+ }
29219
+ const projectId = args.projectId || deps.getActiveProjectId() || undefined;
29220
+ const validSteps = [
29221
+ "prune",
29222
+ "consolidate",
29223
+ "orphans",
29224
+ "duplicates",
29225
+ "backfill"
29226
+ ];
29227
+ const rawSteps = args.steps;
29228
+ const steps = rawSteps?.filter((s) => validSteps.includes(s));
29229
+ if (rawSteps && steps && steps.length < rawSteps.length) {
29230
+ const invalid = rawSteps.filter((s) => !validSteps.includes(s));
29231
+ console.warn(`Unknown cleanup steps ignored: ${invalid.join(", ")}`);
29232
+ }
29233
+ const report = await runMemoryCleanup(client3, workspaceId, projectId, {
29234
+ dryRun: args.dryRun,
29235
+ steps,
29236
+ maxAgeDays: args.maxAgeDays,
29237
+ minClusterSize: args.minClusterSize,
29238
+ orphanAgeDays: args.orphanAgeDays
29239
+ });
29240
+ return {
29241
+ success: report.success,
29242
+ dryRun: report.dryRun,
29243
+ summary: report.summary,
29244
+ errors: report.errors,
29245
+ healthReport: report.healthReport
29246
+ };
29247
+ }
28946
29248
  default:
28947
29249
  throw new Error(`Unknown tool: ${name}`);
28948
29250
  }