@gethmy/mcp 2.4.3 → 2.4.6

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/remote.js CHANGED
@@ -7070,6 +7070,7 @@ __export(exports_context_assembly, {
7070
7070
  mapToContextEntity: () => mapToContextEntity,
7071
7071
  getSessionAssemblyId: () => getSessionAssemblyId,
7072
7072
  getCachedManifest: () => getCachedManifest,
7073
+ expandQuery: () => expandQuery,
7073
7074
  computeRelevanceScore: () => computeRelevanceScore,
7074
7075
  cacheManifest: () => cacheManifest,
7075
7076
  assembleContext: () => assembleContext
@@ -7077,6 +7078,29 @@ __export(exports_context_assembly, {
7077
7078
  function estimateTokens(text) {
7078
7079
  return Math.ceil(text.length / 4);
7079
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
+ }
7080
7104
  function generateAssemblyId() {
7081
7105
  return `ctx_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
7082
7106
  }
@@ -7108,7 +7132,45 @@ function truncateContent(content, maxTokens) {
7108
7132
  }
7109
7133
  return { text: result, truncated: true };
7110
7134
  }
7111
- function computeRelevanceScore(entity, taskContext, cardLabels) {
7135
+ function escapeRegex2(str) {
7136
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7137
+ }
7138
+ function expandQuery(taskContext) {
7139
+ const queries = [taskContext];
7140
+ const lowerQueries = [taskContext.toLowerCase()];
7141
+ const words = taskContext.toLowerCase().split(/\W+/).filter((w) => w.length > 2);
7142
+ const expandableWords = words.filter((w) => QUERY_SYNONYMS[w]);
7143
+ for (const word of expandableWords) {
7144
+ const synonyms = QUERY_SYNONYMS[word];
7145
+ if (!synonyms)
7146
+ continue;
7147
+ const variation = taskContext.replace(new RegExp(`\\b${escapeRegex2(word)}\\b`, "gi"), synonyms[0]);
7148
+ const lowerVariation = variation.toLowerCase();
7149
+ if (lowerVariation !== taskContext.toLowerCase() && !lowerQueries.includes(lowerVariation)) {
7150
+ queries.push(variation);
7151
+ lowerQueries.push(lowerVariation);
7152
+ }
7153
+ if (queries.length >= MAX_QUERY_VARIATIONS)
7154
+ break;
7155
+ }
7156
+ if (words.length >= 3) {
7157
+ const keyPhrases = words.filter((w) => ![
7158
+ "the",
7159
+ "and",
7160
+ "for",
7161
+ "with",
7162
+ "this",
7163
+ "that",
7164
+ "from",
7165
+ "into"
7166
+ ].includes(w)).slice(0, 4).join(" ");
7167
+ if (!lowerQueries.includes(keyPhrases)) {
7168
+ queries.push(keyPhrases);
7169
+ }
7170
+ }
7171
+ return queries.slice(0, MAX_QUERY_VARIATIONS);
7172
+ }
7173
+ function computeRelevanceScore(entity, taskContext, cardLabels, graphRelations) {
7112
7174
  const reasons = [];
7113
7175
  let score = 0;
7114
7176
  const hasRrfScore = entity.rrf_score !== undefined && entity.rrf_score > 0;
@@ -7167,7 +7229,23 @@ function computeRelevanceScore(entity, taskContext, cardLabels) {
7167
7229
  score += 0.1;
7168
7230
  reasons.push("procedure_boost");
7169
7231
  }
7170
- score = Math.min(score, 1);
7232
+ if (graphRelations && graphRelations.length > 0) {
7233
+ const entityRelations = graphRelations.filter((r) => r.source_id === entity.id || r.target_id === entity.id);
7234
+ if (entityRelations.length > 0) {
7235
+ let bestBonus = 0;
7236
+ let bestRelType = "";
7237
+ for (const rel of entityRelations) {
7238
+ const bonus = RELATION_BONUSES[rel.relation_type] ?? 0.1;
7239
+ if (bonus > bestBonus) {
7240
+ bestBonus = bonus;
7241
+ bestRelType = rel.relation_type;
7242
+ }
7243
+ }
7244
+ score += bestBonus;
7245
+ reasons.push(`graph_walk(${bestRelType})`);
7246
+ }
7247
+ }
7248
+ score = Math.max(0, Math.min(score, 1));
7171
7249
  const tierWeight = TIER_WEIGHTS[entity.memory_tier];
7172
7250
  score *= tierWeight;
7173
7251
  return { score, reasons };
@@ -7179,7 +7257,11 @@ async function assembleContext(options) {
7179
7257
  taskContext,
7180
7258
  cardLabels = [],
7181
7259
  tokenBudget = DEFAULT_TOKEN_BUDGET,
7182
- client: client2
7260
+ client: client2,
7261
+ graphWalkEnabled = true,
7262
+ queryExpansionEnabled = true,
7263
+ enableLlmReranking = false,
7264
+ rerankFn
7183
7265
  } = options;
7184
7266
  const assemblyId = generateAssemblyId();
7185
7267
  const manifest = {
@@ -7195,13 +7277,26 @@ async function assembleContext(options) {
7195
7277
  reference: { count: 0, tokens: 0 }
7196
7278
  }
7197
7279
  };
7198
- let candidates = [];
7199
- try {
7200
- const searchResult = await client2.searchMemoryEntities(workspaceId, taskContext, { project_id: projectId, limit: 30 });
7201
- if (searchResult.entities?.length > 0) {
7202
- candidates = searchResult.entities.map(mapToContextEntity);
7280
+ const candidates = [];
7281
+ const queries = queryExpansionEnabled ? expandQuery(taskContext) : [taskContext];
7282
+ const searchResults = await Promise.allSettled(queries.map((query) => client2.searchMemoryEntities(workspaceId, query, {
7283
+ project_id: projectId,
7284
+ limit: 30
7285
+ })));
7286
+ const candidateIds = new Set;
7287
+ for (const result of searchResults) {
7288
+ if (result.status !== "fulfilled")
7289
+ continue;
7290
+ if (result.value.entities?.length > 0) {
7291
+ for (const raw2 of result.value.entities) {
7292
+ const entity = mapToContextEntity(raw2);
7293
+ if (!candidateIds.has(entity.id)) {
7294
+ candidateIds.add(entity.id);
7295
+ candidates.push(entity);
7296
+ }
7297
+ }
7203
7298
  }
7204
- } catch {}
7299
+ }
7205
7300
  if (candidates.length < 10 && projectId) {
7206
7301
  try {
7207
7302
  const listResult = await client2.listMemoryEntities({
@@ -7210,9 +7305,13 @@ async function assembleContext(options) {
7210
7305
  limit: 30
7211
7306
  });
7212
7307
  if (listResult.entities?.length > 0) {
7213
- const existingIds = new Set(candidates.map((c) => c.id));
7214
- const additional = listResult.entities.map(mapToContextEntity).filter((e) => !existingIds.has(e.id));
7215
- candidates.push(...additional);
7308
+ for (const raw2 of listResult.entities) {
7309
+ const entity = mapToContextEntity(raw2);
7310
+ if (!candidateIds.has(entity.id)) {
7311
+ candidateIds.add(entity.id);
7312
+ candidates.push(entity);
7313
+ }
7314
+ }
7216
7315
  }
7217
7316
  } catch {}
7218
7317
  }
@@ -7224,9 +7323,33 @@ async function assembleContext(options) {
7224
7323
  limit: 20
7225
7324
  });
7226
7325
  if (wsResult.entities?.length > 0) {
7227
- const existingIds = new Set(candidates.map((c) => c.id));
7228
- const additional = wsResult.entities.map(mapToContextEntity).filter((e) => !existingIds.has(e.id));
7229
- candidates.push(...additional);
7326
+ for (const raw2 of wsResult.entities) {
7327
+ const entity = mapToContextEntity(raw2);
7328
+ if (!candidateIds.has(entity.id)) {
7329
+ candidateIds.add(entity.id);
7330
+ candidates.push(entity);
7331
+ }
7332
+ }
7333
+ }
7334
+ } catch {}
7335
+ }
7336
+ let graphRelations = [];
7337
+ if (graphWalkEnabled && candidates.length > 0) {
7338
+ try {
7339
+ const seedCandidates = [...candidates].sort((a, b) => (b.rrf_score ?? 0) - (a.rrf_score ?? 0)).slice(0, GRAPH_WALK_SEED_COUNT);
7340
+ const seedIds = seedCandidates.map((c) => c.id);
7341
+ const walkResult = await discoverRelatedContext(client2, seedIds, GRAPH_WALK_MAX_DEPTH, GRAPH_WALK_MAX_ENTITIES, GRAPH_WALK_MIN_CONFIDENCE);
7342
+ graphRelations = walkResult.relations;
7343
+ const newEntityIds = walkResult.entities.filter((e) => !candidateIds.has(e.id)).map((e) => e.id);
7344
+ if (newEntityIds.length > 0) {
7345
+ const fetchResults = await Promise.allSettled(newEntityIds.map((id) => client2.getMemoryEntity(id)));
7346
+ for (const result of fetchResults) {
7347
+ if (result.status !== "fulfilled" || !result.value.entity)
7348
+ continue;
7349
+ const mapped = mapToContextEntity(result.value.entity);
7350
+ candidateIds.add(mapped.id);
7351
+ candidates.push(mapped);
7352
+ }
7230
7353
  }
7231
7354
  } catch {}
7232
7355
  }
@@ -7237,11 +7360,52 @@ async function assembleContext(options) {
7237
7360
  memories: []
7238
7361
  };
7239
7362
  }
7240
- const scored = candidates.map((entity) => {
7241
- const { score, reasons } = computeRelevanceScore(entity, taskContext, cardLabels);
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) => {
7384
+ const { score, reasons } = computeRelevanceScore(entity, taskContext, cardLabels, graphRelations.length > 0 ? graphRelations : undefined);
7242
7385
  return { entity, score, reasons };
7243
7386
  });
7244
7387
  scored.sort((a, b) => b.score - a.score);
7388
+ if (enableLlmReranking && rerankFn && scored.length >= RERANK_MIN_CANDIDATES) {
7389
+ const topN = scored.slice(0, RERANK_TOP_N);
7390
+ const scoreRange = topN[0].score - topN[topN.length - 1].score;
7391
+ if (scoreRange <= RERANK_CLUSTER_THRESHOLD) {
7392
+ try {
7393
+ const rerankCandidates = topN.map((s) => ({
7394
+ id: s.entity.id,
7395
+ title: s.entity.title,
7396
+ snippet: s.entity.content.slice(0, 200)
7397
+ }));
7398
+ const rerankedIds = await rerankFn(taskContext, rerankCandidates);
7399
+ const idOrder = new Map(rerankedIds.map((id, i) => [id, i]));
7400
+ topN.sort((a, b) => {
7401
+ const aIdx = idOrder.get(a.entity.id) ?? 999;
7402
+ const bIdx = idOrder.get(b.entity.id) ?? 999;
7403
+ return aIdx - bIdx;
7404
+ });
7405
+ scored.splice(0, topN.length, ...topN);
7406
+ } catch {}
7407
+ }
7408
+ }
7245
7409
  const procedureBudget = Math.floor(tokenBudget * PROCEDURE_BUDGET_FRACTION);
7246
7410
  const remainingBudget = tokenBudget - procedureBudget;
7247
7411
  const tierBudgets = {
@@ -7553,7 +7717,7 @@ async function recordContextFeedback(client2, cardId, sessionStatus, progressPer
7553
7717
  sessionAssemblyMap.delete(cardId);
7554
7718
  return { adjusted };
7555
7719
  }
7556
- 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;
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;
7557
7721
  var init_context_assembly = __esm(() => {
7558
7722
  init_dist();
7559
7723
  TIER_WEIGHTS = {
@@ -7566,6 +7730,33 @@ var init_context_assembly = __esm(() => {
7566
7730
  episode: 0.3,
7567
7731
  draft: 0.1
7568
7732
  };
7733
+ RELATION_BONUSES = {
7734
+ depends_on: 0.15,
7735
+ resolved_by: 0.2,
7736
+ relates_to: 0.1,
7737
+ implements: 0.15,
7738
+ blocks: 0.15,
7739
+ references: 0.1,
7740
+ extends: 0.1,
7741
+ caused_by: 0.15
7742
+ };
7743
+ QUERY_SYNONYMS = {
7744
+ auth: ["authentication", "authorization", "session"],
7745
+ authentication: ["auth", "session", "sign-in"],
7746
+ login: ["sign-in", "authentication", "session"],
7747
+ bug: ["error", "issue", "defect", "problem"],
7748
+ error: ["exception", "failure", "issue"],
7749
+ fix: ["resolve", "patch", "repair", "correct"],
7750
+ deploy: ["deployment", "release", "ship", "publish"],
7751
+ test: ["testing", "spec", "assertion", "verify"],
7752
+ config: ["configuration", "settings", "setup"],
7753
+ db: ["database", "storage", "persistence"],
7754
+ database: ["storage", "persistence", "data store"],
7755
+ api: ["endpoint", "route", "service"],
7756
+ ui: ["frontend", "component", "view"],
7757
+ perf: ["performance", "speed", "latency"],
7758
+ performance: ["speed", "latency", "optimization"]
7759
+ };
7569
7760
  manifestCache = new Map;
7570
7761
  sessionAssemblyMap = new Map;
7571
7762
  });
@@ -7694,7 +7885,11 @@ ${card.description}`);
7694
7885
  roleFraming.focus.forEach((f) => {
7695
7886
  sections.push(`- ${f}`);
7696
7887
  });
7697
- 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 \u2014 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 \u2014 optimistic updates depend on action ordering"`);
7890
+ sections.push(` - GOOD: "Mobile bottom bar is 64px, overlaps fixed-position drawers \u2014 always add pb-16 to drawer content"`);
7891
+ sections.push(` - BAD: "Fixed the login button" (no reusable knowledge \u2014 the fix is in the code)`);
7892
+ sections.push(` - BAD: "Completed card #42" (ephemeral, auto-tracked by session)`);
7698
7893
  sections.push(`
7699
7894
  ## Suggested Outputs`);
7700
7895
  roleFraming.outputSuggestions.forEach((s) => {
@@ -26137,9 +26332,20 @@ class HarmonyApiClient {
26137
26332
  },
26138
26333
  body: options?.rawBody ?? (body ? JSON.stringify(body) : undefined)
26139
26334
  });
26140
- const data = await response.json();
26335
+ const text = await response.text();
26336
+ const responseContentType = response.headers.get("content-type") || "";
26337
+ const looksLikeJson = responseContentType.includes("application/json");
26338
+ let data = null;
26339
+ let parseError = null;
26340
+ if (text) {
26341
+ try {
26342
+ data = JSON.parse(text);
26343
+ } catch (err) {
26344
+ parseError = err instanceof Error ? err : new Error(String(err));
26345
+ }
26346
+ }
26141
26347
  if (!response.ok) {
26142
- const errorMsg = data.error || `API error: ${response.status}`;
26348
+ const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
26143
26349
  if (!isRetryableError(null, response.status)) {
26144
26350
  throw new Error(errorMsg);
26145
26351
  }
@@ -26150,6 +26356,9 @@ class HarmonyApiClient {
26150
26356
  }
26151
26357
  throw lastError;
26152
26358
  }
26359
+ if (parseError) {
26360
+ throw new Error(`API returned ${response.status} with invalid JSON body: ${parseError.message}`);
26361
+ }
26153
26362
  return data;
26154
26363
  } catch (error48) {
26155
26364
  lastError = error48 instanceof Error ? error48 : new Error(String(error48));
@@ -26298,6 +26507,9 @@ class HarmonyApiClient {
26298
26507
  async createLabel(projectId, data) {
26299
26508
  return this.request("POST", "/labels", { projectId, ...data });
26300
26509
  }
26510
+ async deleteLabel(labelId) {
26511
+ return this.request("DELETE", `/labels/${labelId}`);
26512
+ }
26301
26513
  async createSubtask(cardId, title) {
26302
26514
  return this.request("POST", "/subtasks", { cardId, title });
26303
26515
  }
@@ -26316,6 +26528,12 @@ class HarmonyApiClient {
26316
26528
  async endAgentSession(cardId, data) {
26317
26529
  return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
26318
26530
  }
26531
+ async flushActivityLog(cardId, data) {
26532
+ return this.request("POST", `/cards/${cardId}/agent-activity-log`, data);
26533
+ }
26534
+ async getActivityLog(cardId, sessionId) {
26535
+ return this.request("GET", `/cards/${cardId}/agent-activity-log?sessionId=${sessionId}`);
26536
+ }
26319
26537
  async getAgentSession(cardId, options) {
26320
26538
  const params = new URLSearchParams;
26321
26539
  if (options?.includeEnded)
@@ -26872,13 +27090,13 @@ function levenshteinSimilarity(a, b) {
26872
27090
  const maxLen = Math.max(sa.length, sb.length);
26873
27091
  return 1 - matrix[sa.length][sb.length] / maxLen;
26874
27092
  }
26875
- async function extractMidSessionLearnings(client3, ctx) {
27093
+ async function extractMidSessionLearnings(_client, ctx) {
26876
27094
  const workspaceId = getActiveWorkspaceId();
26877
27095
  if (!workspaceId)
26878
27096
  return { count: 0, entityIds: [] };
26879
- const projectId = getActiveProjectId() || undefined;
27097
+ const _projectId = getActiveProjectId() || undefined;
26880
27098
  const now = Date.now();
26881
- const entityIds = [];
27099
+ const _entityIds = [];
26882
27100
  const history = sessionTaskHistory.get(ctx.cardId);
26883
27101
  if (ctx.currentTask) {
26884
27102
  const previousTask = history?.lastTask || "";
@@ -26911,81 +27129,22 @@ async function extractMidSessionLearnings(client3, ctx) {
26911
27129
  }
26912
27130
  }
26913
27131
  if (ctx.status === "blocked" && ctx.blockers?.length) {
26914
- for (const blocker of ctx.blockers) {
26915
- try {
26916
- const result = await client3.createMemoryEntity({
26917
- workspace_id: workspaceId,
26918
- project_id: projectId,
26919
- type: "error",
26920
- scope: "project",
26921
- memory_tier: "draft",
26922
- title: `Blocker (mid-session): ${blocker.slice(0, 100)}`,
26923
- content: `Encountered while working on "${ctx.cardTitle}":
26924
-
26925
- ${blocker}
26926
-
26927
- Agent: ${ctx.agentName}
26928
- Progress: ${ctx.progressPercent ?? "unknown"}%`,
26929
- confidence: 0.5,
26930
- tags: ["auto-extracted", "blocker", "mid-session"],
26931
- metadata: {
26932
- source: "mid_session",
26933
- card_id: ctx.cardId
26934
- },
26935
- agent_identifier: ctx.agentIdentifier
26936
- });
26937
- const entity = result.entity;
26938
- if (entity?.id)
26939
- entityIds.push(entity.id);
26940
- } catch {}
26941
- }
26942
27132
  sessionTaskHistory.set(ctx.cardId, {
26943
27133
  lastTask: ctx.currentTask || "",
26944
27134
  lastExtractionAt: now,
26945
27135
  steps: history?.steps || []
26946
27136
  });
26947
- return { count: entityIds.length, entityIds };
27137
+ return { count: 0, entityIds: [] };
26948
27138
  }
26949
27139
  if (ctx.currentTask) {
26950
- const previousTask = history?.lastTask || "";
26951
- const similarity = levenshteinSimilarity(previousTask, ctx.currentTask);
26952
- if (similarity < 0.6 && previousTask.length > 0) {
26953
- try {
26954
- const result = await client3.createMemoryEntity({
26955
- workspace_id: workspaceId,
26956
- project_id: projectId,
26957
- type: "context",
26958
- scope: "project",
26959
- memory_tier: "draft",
26960
- title: `Task transition: ${ctx.cardTitle}`,
26961
- content: `Agent transitioned tasks on "${ctx.cardTitle}".
26962
-
26963
- Previous: ${previousTask}
26964
- Current: ${ctx.currentTask}
26965
- Progress: ${ctx.progressPercent ?? "unknown"}%`,
26966
- confidence: 0.5,
26967
- tags: ["auto-extracted", "task-transition", "mid-session"],
26968
- metadata: {
26969
- source: "mid_session",
26970
- card_id: ctx.cardId,
26971
- previous_task: previousTask,
26972
- current_task: ctx.currentTask
26973
- },
26974
- agent_identifier: ctx.agentIdentifier
26975
- });
26976
- const entity = result.entity;
26977
- if (entity?.id)
26978
- entityIds.push(entity.id);
26979
- } catch {}
26980
- }
26981
27140
  const currentHistory = sessionTaskHistory.get(ctx.cardId);
26982
27141
  sessionTaskHistory.set(ctx.cardId, {
26983
27142
  lastTask: ctx.currentTask,
26984
- lastExtractionAt: entityIds.length > 0 ? now : currentHistory?.lastExtractionAt ?? 0,
27143
+ lastExtractionAt: currentHistory?.lastExtractionAt ?? 0,
26985
27144
  steps: currentHistory?.steps || []
26986
27145
  });
26987
27146
  }
26988
- return { count: entityIds.length, entityIds };
27147
+ return { count: 0, entityIds: [] };
26989
27148
  }
26990
27149
  function clearMidSessionTracking(cardId) {
26991
27150
  sessionTaskHistory.delete(cardId);
@@ -27169,49 +27328,60 @@ async function extractLearnings(client3, session) {
27169
27328
  Related: ${relatedEntityTitles.map((t) => `[[${t}]]`).join(", ")}` : "";
27170
27329
  if (session.blockers && session.blockers.length > 0) {
27171
27330
  for (const blocker of session.blockers) {
27172
- learnings.push({
27173
- title: `Blocker: ${blocker.slice(0, 100)}`,
27174
- content: `Encountered while working on "${session.cardTitle}":
27331
+ if (blocker.length < 80)
27332
+ continue;
27333
+ let isDuplicate = false;
27334
+ try {
27335
+ const similar = await findSimilarEntities(client3, blocker.slice(0, 200), blocker, workspaceId, { projectId, limit: 3, minRrfScore: 0.05 });
27336
+ isDuplicate = similar.some((e) => e.type === "error" && (e.rrf_score ?? 0) >= 0.06);
27337
+ } catch {}
27338
+ if (!isDuplicate) {
27339
+ learnings.push({
27340
+ title: `Blocker: ${blocker.slice(0, 100)}`,
27341
+ content: `Encountered while working on "${session.cardTitle}":
27175
27342
 
27176
27343
  ${blocker}
27177
27344
 
27178
27345
  Agent: ${session.agentName}
27179
27346
  Session status: ${session.status}`,
27180
- type: "error",
27181
- tier: "reference",
27182
- confidence: 0.7,
27183
- tags: ["auto-extracted", "blocker", ...session.cardLabels.slice(0, 3)],
27184
- metadata: {
27185
- source: "active_learning",
27186
- card_id: session.cardId
27187
- }
27188
- });
27347
+ type: "error",
27348
+ tier: "episode",
27349
+ confidence: 0.6,
27350
+ tags: [
27351
+ "auto-extracted",
27352
+ "blocker",
27353
+ ...session.cardLabels.slice(0, 3)
27354
+ ],
27355
+ metadata: {
27356
+ source: "active_learning",
27357
+ card_id: session.cardId
27358
+ }
27359
+ });
27360
+ }
27189
27361
  }
27190
27362
  }
27191
- const hasMeaningfulContent = (session.blockers?.length ?? 0) > 0 || session.status === "paused" || (session.cardSubtasks?.length ?? 0) > 0 && session.cardSubtasks?.some((s) => !s.done);
27192
- if (session.status === "completed" && hasMeaningfulContent) {
27363
+ if (session.status === "paused" && (session.blockers?.length ?? 0) > 0) {
27193
27364
  const durationInfo = session.sessionDurationMs ? `
27194
27365
  Duration: ${Math.round(session.sessionDurationMs / 60000)} minutes` : "";
27195
27366
  learnings.push({
27196
- title: `Session: ${session.cardTitle}`,
27367
+ title: `Paused: ${session.cardTitle}`,
27197
27368
  content: [
27198
- `Completed work on "${session.cardTitle}".`,
27199
- session.currentTask ? `Final task: ${session.currentTask}` : "",
27369
+ `Paused work on "${session.cardTitle}".`,
27370
+ session.currentTask ? `Last task: ${session.currentTask}` : "",
27200
27371
  session.progressPercent !== undefined ? `Progress: ${session.progressPercent}%` : "",
27201
27372
  durationInfo,
27202
- session.cardLabels.length > 0 ? `Labels: ${session.cardLabels.join(", ")}` : "",
27203
- session.blockers?.length ? `Blockers encountered: ${session.blockers.join("; ")}` : "",
27373
+ session.blockers?.length ? `Blockers: ${session.blockers.join("; ")}` : "",
27204
27374
  `
27205
27375
  Agent: ${session.agentName}`,
27206
27376
  wikiLinksLine
27207
27377
  ].filter(Boolean).join(`
27208
27378
  `),
27209
27379
  type: "lesson",
27210
- tier: "episode",
27211
- confidence: 0.7,
27380
+ tier: "draft",
27381
+ confidence: 0.6,
27212
27382
  tags: [
27213
27383
  "auto-extracted",
27214
- "session-summary",
27384
+ "session-paused",
27215
27385
  ...session.cardLabels.slice(0, 3)
27216
27386
  ],
27217
27387
  metadata: {
@@ -27220,35 +27390,14 @@ Agent: ${session.agentName}`,
27220
27390
  }
27221
27391
  });
27222
27392
  }
27223
- const hasBugLabel = session.cardLabels.some((l) => ["bug", "fix", "hotfix", "defect", "error"].includes(l.toLowerCase()));
27224
- if (hasBugLabel && session.status === "completed") {
27225
- learnings.push({
27226
- title: `Solution: ${session.cardTitle}`,
27227
- content: [
27228
- `Resolved bug: "${session.cardTitle}"`,
27229
- session.currentTask ? `
27230
- Approach: ${session.currentTask}` : "",
27231
- `
27232
- Agent: ${session.agentName}`,
27233
- wikiLinksLine
27234
- ].filter(Boolean).join(`
27235
- `),
27236
- type: "solution",
27237
- tier: "reference",
27238
- confidence: 0.8,
27239
- tags: ["auto-extracted", "bug-fix", ...session.cardLabels.slice(0, 3)],
27240
- metadata: {
27241
- source: "active_learning",
27242
- card_id: session.cardId,
27243
- auto_confidence: true
27244
- }
27245
- });
27246
- }
27247
27393
  const entityIds = [];
27248
27394
  const stepHistory = sessionTaskHistory.get(session.cardId);
27249
- const hasEnoughSteps = stepHistory && stepHistory.steps.length >= 2;
27395
+ const MIN_PROCEDURE_STEPS = 5;
27396
+ const MIN_PROCEDURE_DURATION_MS = 10 * 60 * 1000;
27397
+ const hasEnoughSteps = stepHistory && stepHistory.steps.length >= MIN_PROCEDURE_STEPS;
27398
+ const hasMinDuration = (session.sessionDurationMs ?? 0) >= MIN_PROCEDURE_DURATION_MS;
27250
27399
  const isSuccessful = session.status === "completed" && (session.progressPercent === undefined || session.progressPercent >= 85) && !session.blockers?.length;
27251
- if (isSuccessful && hasEnoughSteps) {
27400
+ if (isSuccessful && hasEnoughSteps && hasMinDuration) {
27252
27401
  const procedureResult = await extractOrReinforceProcedure(client3, session, stepHistory.steps, workspaceId, projectId, wikiLinksLine);
27253
27402
  if (procedureResult) {
27254
27403
  if (procedureResult.mode === "created") {
@@ -27292,218 +27441,9 @@ Agent: ${session.agentName}`,
27292
27441
  if (createdPairs.length >= 2) {
27293
27442
  linkSessionEntities(client3, createdPairs, workspaceId, projectId).catch(() => {});
27294
27443
  }
27295
- if (entityIds.length > 0) {
27296
- detectAndCreatePatterns(client3, entityIds, session, workspaceId, projectId).catch(() => {});
27297
- }
27298
- if (createdPairs.length > 0) {
27299
- detectCausalPatterns(client3, createdPairs, session, workspaceId, projectId).catch(() => {});
27300
- }
27301
27444
  clearMidSessionTracking(session.cardId);
27302
27445
  return { count: entityIds.length, entityIds };
27303
27446
  }
27304
- var PATTERN_THRESHOLD = 3;
27305
- async function detectAndCreatePatterns(client3, newEntityIds, session, workspaceId, projectId) {
27306
- const patternEntityIds = [];
27307
- for (const newEntityId of newEntityIds) {
27308
- try {
27309
- const { entity: rawEntity } = await client3.getMemoryEntity(newEntityId);
27310
- const entity = rawEntity;
27311
- if (!entity?.type)
27312
- continue;
27313
- const similar = await findSimilarEntities(client3, entity.title, entity.content, workspaceId, { projectId, limit: 30, minRrfScore: 0.01 });
27314
- const existing = similar.filter((c) => !newEntityIds.includes(c.id) && c.type === entity.type);
27315
- if (existing.length < PATTERN_THRESHOLD)
27316
- continue;
27317
- const memberTitles = [
27318
- entity.title,
27319
- ...existing.slice(0, 4).map((e) => e.title)
27320
- ];
27321
- const patternTitle = `Pattern: recurring ${entity.type} (${existing.length + 1} instances)`;
27322
- const { entities: existingPatterns } = await client3.listMemoryEntities({
27323
- workspace_id: workspaceId,
27324
- project_id: projectId,
27325
- type: "pattern",
27326
- limit: 10
27327
- });
27328
- const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_type === entity.type);
27329
- let patternId = null;
27330
- if (matchingPattern) {
27331
- patternId = matchingPattern.id;
27332
- await client3.updateMemoryEntity(patternId, {
27333
- content: `Recurring pattern: ${entity.type} entities appearing ${existing.length + 1} times.
27334
-
27335
- Members:
27336
- ${memberTitles.map((t) => `- ${t}`).join(`
27337
- `)}
27338
-
27339
- Last updated: ${new Date().toISOString()}`,
27340
- metadata: {
27341
- pattern_count: existing.length + 1,
27342
- pattern_type: entity.type,
27343
- last_updated: new Date().toISOString()
27344
- }
27345
- });
27346
- } else {
27347
- const result = await client3.createMemoryEntity({
27348
- workspace_id: workspaceId,
27349
- project_id: projectId,
27350
- type: "pattern",
27351
- scope: "project",
27352
- memory_tier: "reference",
27353
- title: patternTitle,
27354
- content: `Recurring pattern: ${entity.type} entities detected ${existing.length + 1} times.
27355
-
27356
- Members:
27357
- ${memberTitles.map((t) => `- ${t}`).join(`
27358
- `)}`,
27359
- confidence: 0.75,
27360
- tags: ["auto-extracted", "pattern", entity.type],
27361
- metadata: {
27362
- source: "pattern_detection",
27363
- pattern_type: entity.type,
27364
- pattern_count: existing.length + 1
27365
- },
27366
- agent_identifier: session.agentIdentifier
27367
- });
27368
- const created = result.entity;
27369
- if (created?.id) {
27370
- patternId = created.id;
27371
- patternEntityIds.push(patternId);
27372
- }
27373
- }
27374
- if (!patternId)
27375
- continue;
27376
- const toLink = [newEntityId, ...existing.slice(0, 4).map((e) => e.id)];
27377
- for (const sourceId of toLink) {
27378
- try {
27379
- await client3.createMemoryRelation({
27380
- source_id: sourceId,
27381
- target_id: patternId,
27382
- relation_type: "part_of",
27383
- confidence: 0.75
27384
- });
27385
- } catch {}
27386
- }
27387
- } catch {}
27388
- }
27389
- return patternEntityIds;
27390
- }
27391
- var CAUSAL_PATTERN_THRESHOLD = 3;
27392
- async function detectCausalPatterns(client3, createdPairs, session, workspaceId, projectId) {
27393
- const patternIds = [];
27394
- const errors3 = createdPairs.filter((p) => p.learning.type === "error");
27395
- const solutions = createdPairs.filter((p) => p.learning.type === "solution");
27396
- if (errors3.length === 0 || solutions.length === 0)
27397
- return patternIds;
27398
- for (const errorPair of errors3) {
27399
- try {
27400
- const similarErrors = await findSimilarEntities(client3, errorPair.learning.title, errorPair.learning.content, workspaceId, {
27401
- projectId,
27402
- limit: 20,
27403
- minRrfScore: 0.03,
27404
- excludeIds: createdPairs.map((p) => p.id),
27405
- type: "error"
27406
- });
27407
- const resolvedErrors = [];
27408
- for (const similar of similarErrors.slice(0, 10)) {
27409
- try {
27410
- const { outgoing } = await client3.getRelatedEntities(similar.id);
27411
- const resolvedByRel = outgoing.find((r) => r.relation_type === "resolved_by");
27412
- if (resolvedByRel) {
27413
- resolvedErrors.push({
27414
- errorId: similar.id,
27415
- errorTitle: similar.title,
27416
- solutionTitle: resolvedByRel.target_title || "unknown"
27417
- });
27418
- }
27419
- } catch {}
27420
- }
27421
- if (resolvedErrors.length + 1 < CAUSAL_PATTERN_THRESHOLD)
27422
- continue;
27423
- const { entities: existingPatterns } = await client3.listMemoryEntities({
27424
- workspace_id: workspaceId,
27425
- project_id: projectId,
27426
- type: "pattern",
27427
- limit: 10
27428
- });
27429
- const matchingPattern = existingPatterns.find((p) => p.metadata?.pattern_chain_type === "error_resolved_by_solution");
27430
- if (matchingPattern) {
27431
- await client3.updateMemoryEntity(matchingPattern.id, {
27432
- content: [
27433
- `Recurring error\u2192solution chain detected (${resolvedErrors.length + 1} instances).`,
27434
- "",
27435
- "## Error\u2192Solution Pairs",
27436
- `- ${errorPair.learning.title} \u2192 ${solutions[0].learning.title}`,
27437
- ...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} \u2192 ${r.solutionTitle}`),
27438
- "",
27439
- `Last updated: ${new Date().toISOString()}`
27440
- ].join(`
27441
- `),
27442
- metadata: {
27443
- pattern_chain_type: "error_resolved_by_solution",
27444
- pattern_count: resolvedErrors.length + 1,
27445
- last_updated: new Date().toISOString()
27446
- }
27447
- });
27448
- for (const pair of [errorPair, solutions[0]]) {
27449
- try {
27450
- await client3.createMemoryRelation({
27451
- source_id: pair.id,
27452
- target_id: matchingPattern.id,
27453
- relation_type: "part_of",
27454
- confidence: 0.75
27455
- });
27456
- } catch {}
27457
- }
27458
- } else {
27459
- const result = await client3.createMemoryEntity({
27460
- workspace_id: workspaceId,
27461
- project_id: projectId,
27462
- type: "pattern",
27463
- scope: "project",
27464
- memory_tier: "reference",
27465
- title: `Pattern: recurring error\u2192solution chain (${resolvedErrors.length + 1} instances)`,
27466
- content: [
27467
- `Recurring error\u2192solution chain detected across ${resolvedErrors.length + 1} sessions.`,
27468
- "",
27469
- "## Error\u2192Solution Pairs",
27470
- `- ${errorPair.learning.title} \u2192 ${solutions[0].learning.title}`,
27471
- ...resolvedErrors.slice(0, 5).map((r) => `- ${r.errorTitle} \u2192 ${r.solutionTitle}`)
27472
- ].join(`
27473
- `),
27474
- confidence: 0.8,
27475
- tags: ["auto-extracted", "pattern", "causal-chain"],
27476
- metadata: {
27477
- source: "causal_pattern_detection",
27478
- pattern_chain_type: "error_resolved_by_solution",
27479
- pattern_count: resolvedErrors.length + 1
27480
- },
27481
- agent_identifier: session.agentIdentifier
27482
- });
27483
- const created = result.entity;
27484
- if (created?.id) {
27485
- patternIds.push(created.id);
27486
- const memberIds = [
27487
- errorPair.id,
27488
- solutions[0].id,
27489
- ...resolvedErrors.slice(0, 4).map((r) => r.errorId)
27490
- ];
27491
- for (const memberId of memberIds) {
27492
- try {
27493
- await client3.createMemoryRelation({
27494
- source_id: memberId,
27495
- target_id: created.id,
27496
- relation_type: "part_of",
27497
- confidence: 0.75
27498
- });
27499
- } catch {}
27500
- }
27501
- }
27502
- }
27503
- } catch {}
27504
- }
27505
- return patternIds;
27506
- }
27507
27447
  async function detectContradictions(client3, entityId, entityType, title, content, tags, workspaceId, projectId) {
27508
27448
  if (!CONTRADICTION_TYPES.has(entityType))
27509
27449
  return [];
@@ -27703,7 +27643,7 @@ async function autoEndSession(client3, cardId, status) {
27703
27643
  // src/consolidation.ts
27704
27644
  async function consolidateMemories(client3, workspaceId, projectId, options) {
27705
27645
  const dryRun = options?.dryRun !== false;
27706
- const minClusterSize = options?.minClusterSize ?? 2;
27646
+ const minClusterSize = options?.minClusterSize ?? 3;
27707
27647
  const result = {
27708
27648
  consolidated: 0,
27709
27649
  clustersFound: 0,
@@ -27768,12 +27708,7 @@ async function consolidateMemories(client3, workspaceId, projectId, options) {
27768
27708
  result.clustersFound++;
27769
27709
  const mergedTitle = deriveClusterTitle(cluster, type);
27770
27710
  const memberTitles = cluster.map((e) => e.title);
27771
- const mergedContent = [
27772
- `Consolidated from ${cluster.length} ${type} memories:
27773
- `,
27774
- ...cluster.map((e) => `- **${e.title}**: ${e.content.slice(0, 200)}`)
27775
- ].join(`
27776
- `);
27711
+ const mergedContent = synthesizeClusterContent(cluster, type);
27777
27712
  const maxConfidence = Math.max(...cluster.map((e) => e.confidence));
27778
27713
  const allTags = [...new Set(cluster.flatMap((e) => e.tags || []))];
27779
27714
  const detail = {
@@ -27835,6 +27770,60 @@ async function consolidateMemories(client3, workspaceId, projectId, options) {
27835
27770
  }
27836
27771
  return result;
27837
27772
  }
27773
+ function synthesizeClusterContent(cluster, type) {
27774
+ const SKIP_PATTERNS = [
27775
+ /^##\s/,
27776
+ /^Agent:/,
27777
+ /^Duration:/,
27778
+ /^Labels:/,
27779
+ /^Progress:/,
27780
+ /^Session status:/,
27781
+ /^Completed at/,
27782
+ /^Final state:/,
27783
+ /^Related:/,
27784
+ /^When working on:/,
27785
+ /^\d+\.\s+.+\(\d+%,\s*\+\d+%\)/,
27786
+ /^Last updated:/,
27787
+ /^Recurring pattern:/,
27788
+ /^Consolidated from/
27789
+ ];
27790
+ const seenLines = new Set;
27791
+ const knowledgeLines = [];
27792
+ for (const entity of cluster) {
27793
+ const lines = entity.content.split(`
27794
+ `).map((l) => l.trim());
27795
+ for (const line of lines) {
27796
+ if (!line || line.length < 20)
27797
+ continue;
27798
+ if (SKIP_PATTERNS.some((p) => p.test(line)))
27799
+ continue;
27800
+ const normalized = line.toLowerCase().replace(/[*_`#[\]]/g, "").trim();
27801
+ if (seenLines.has(normalized))
27802
+ continue;
27803
+ seenLines.add(normalized);
27804
+ knowledgeLines.push(line);
27805
+ }
27806
+ }
27807
+ if (knowledgeLines.length === 0) {
27808
+ return `${cluster.length} related ${type} entities consolidated. Original titles:
27809
+ ${cluster.map((e) => `- ${e.title}`).join(`
27810
+ `)}`;
27811
+ }
27812
+ const MAX_CHARS = 1600;
27813
+ const result = [
27814
+ `Consolidated knowledge from ${cluster.length} ${type} entities:
27815
+ `
27816
+ ];
27817
+ let charCount = result[0].length;
27818
+ for (const line of knowledgeLines) {
27819
+ if (charCount + line.length + 3 > MAX_CHARS)
27820
+ break;
27821
+ result.push(`- ${line}`);
27822
+ charCount += line.length + 3;
27823
+ }
27824
+ return result.join(`
27825
+ `);
27826
+ }
27838
27827
  function deriveClusterTitle(cluster, type) {
27839
27828
  const stopWords = new Set([
27840
27829
  "the",
@@ -27889,9 +27878,9 @@ function deriveClusterTitle(cluster, type) {
27889
27878
  wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
27890
27879
  }
27891
27880
  }
27892
- const topWords = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([word]) => word);
27893
- const suffix = topWords.length > 0 ? topWords.join(", ") : "various";
27894
- return `Consolidated ${type}: ${suffix}`;
27881
+ const topWords = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 4).map(([word]) => word[0].toUpperCase() + word.slice(1));
27882
+ const suffix = topWords.length > 0 ? topWords.join(" / ") : "Various";
27883
+ return `${type[0].toUpperCase() + type.slice(1)}: ${suffix}`;
27895
27884
  }
27896
27885
 
27897
27886
  // src/server.ts
@@ -27967,47 +27956,1000 @@ async function runLifecycleMaintenance(client3, workspaceId, projectId) {
27967
27956
  return result;
27968
27957
  }
27969
27958
 
27970
- // src/server.ts
27971
- var memorySessions = new Map;
27972
- function initMemorySession(cardId, agentIdentifier, agentName) {
27973
- memorySessions.set(cardId, {
27974
- cardId,
27975
- agentIdentifier,
27976
- agentName,
27977
- memoryReadCount: 0,
27978
- pendingActions: [],
27979
- allActions: [],
27980
- dirty: false
27981
- });
27982
- }
27983
- function getMemorySession(cardId) {
27984
- return memorySessions.get(cardId);
27985
- }
27986
- function appendMemoryAction(cardId, action) {
27987
- const session = memorySessions.get(cardId);
27988
- if (!session)
27989
- return;
27990
- const truncated = action.length > 512 ? action.slice(0, 509) + "..." : action;
27991
- const entry = { action: truncated, ts: new Date().toISOString() };
27992
- session.pendingActions.push(entry);
27993
- session.dirty = true;
27994
- }
27995
- function incrementMemoryReads(cardId) {
27996
- const session = memorySessions.get(cardId);
27997
- if (!session)
27998
- return;
27999
- session.memoryReadCount++;
28000
- session.dirty = true;
27959
+ // src/memory-audit.ts
27960
+ init_dist();
27961
+ var EMBEDDINGS_MIGRATION_AT = Date.parse("2026-02-18T00:00:00Z");
27962
+ var MS_PER_DAY = 1000 * 60 * 60 * 24;
27963
+ var BATCH_SIZE = 100;
27964
+ var CONCURRENCY_LIMIT = 5;
27965
+ var BOILERPLATE_PATTERNS = [
27966
+ /^todo:?$/i,
27967
+ /^placeholder/i,
27968
+ /^\.\.\.$/,
27969
+ /^untitled/i,
27970
+ /^(note|memo|draft)\s*\d*$/i,
27971
+ /^task transition:/i
27972
+ ];
27973
+ function isBoilerplate(title, content) {
27974
+ const t = title.trim();
27975
+ const c = content.trim();
27976
+ if (c.length === 0)
27977
+ return true;
27978
+ for (const pat of BOILERPLATE_PATTERNS) {
27979
+ if (pat.test(t))
27980
+ return true;
27981
+ }
27982
+ return false;
28001
27983
  }
28002
- async function flushMemoryActions(client3, cardId) {
28003
- const session = memorySessions.get(cardId);
28004
- if (!session || !session.dirty)
28005
- return;
27984
+ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraftAgeDays) {
27985
+ const now = Date.now();
27986
+ const ageDays = (now - Date.parse(entity.created_at)) / MS_PER_DAY;
27987
+ const effectiveLastAccess = entity.last_accessed_at ?? entity.created_at;
27988
+ const lifecycle2 = evaluateLifecycle({
27989
+ memory_tier: entity.memory_tier,
27990
+ confidence: entity.confidence,
27991
+ access_count: entity.access_count,
27992
+ last_accessed_at: effectiveLastAccess,
27993
+ created_at: entity.created_at
27994
+ });
27995
+ const reasons = [];
27996
+ const legacyReasons = [];
27997
+ const confidence = Math.max(0, Math.min(1, entity.confidence)) * 25;
27998
+ const decay = Math.max(0, Math.min(1, lifecycle2.decay.score)) * 20;
27999
+ if (lifecycle2.decay.score < 0.2)
28000
+ reasons.push(`decay score ${lifecycle2.decay.score.toFixed(2)}`);
28001
+ const hasEmbedding = entity.embedding != null;
28002
+ const hasTags = (entity.tags?.length || 0) >= 1;
28003
+ const hasRelations = relationCount > 0;
28004
+ let structural = 0;
28005
+ if (hasEmbedding)
28006
+ structural += 6;
28007
+ if (hasTags)
28008
+ structural += 4;
28009
+ if (hasRelations)
28010
+ structural += 5;
28011
+ if (!hasEmbedding)
28012
+ reasons.push("no embedding");
28013
+ if (!hasTags)
28014
+ reasons.push("no tags");
28015
+ if (!hasRelations)
28016
+ reasons.push("no relations");
28017
+ let content = 0;
28018
+ const contentLen = entity.content?.length || 0;
28019
+ const boilerplate = isBoilerplate(entity.title, entity.content);
28020
+ if (boilerplate) {
28021
+ reasons.push("boilerplate title/content");
28022
+ } else {
28023
+ if (contentLen >= 80)
28024
+ content += 8;
28025
+ const titleOk = entity.title.trim().length >= 4 && !/^(untitled|draft|note)\b/i.test(entity.title.trim());
28026
+ if (titleOk)
28027
+ content += 4;
28028
+ content += 3;
28029
+ if (contentLen < 80)
28030
+ reasons.push(`thin content (${contentLen} chars)`);
28031
+ }
28032
+ let tierAgeFit = 15;
28033
+ if (entity.memory_tier === "draft" && ageDays > 60 && !entity.promoted_from_id) {
28034
+ tierAgeFit = 0;
28035
+ reasons.push("stuck draft >60d never promoted");
28036
+ } else if (entity.memory_tier === "draft" && (entity.access_count || 0) === 0 && ageDays > 2) {
28037
+ tierAgeFit = 5;
28038
+ reasons.push("draft >2d with zero access");
28039
+ }
28040
+ if (entity.promoted_from_id) {
28041
+ tierAgeFit = Math.min(15, tierAgeFit + 5);
28042
+ }
28043
+ const access = Math.min(10, Math.log10((entity.access_count || 0) + 1) * 5);
28044
+ if (entity.access_count === 0 && ageDays > 14)
28045
+ reasons.push("never accessed");
28046
+ const raw2 = confidence + decay + structural + content + tierAgeFit + access;
28047
+ const score = Math.round(Math.max(0, Math.min(100, raw2)));
28048
+ let legacy = false;
28049
+ if (entity.confidence === 1 && entity.access_count === 0 && ageDays > 30) {
28050
+ legacy = true;
28051
+ legacyReasons.push("default confidence never validated");
28052
+ }
28053
+ if (!hasEmbedding && Date.parse(entity.created_at) < EMBEDDINGS_MIGRATION_AT) {
28054
+ legacy = true;
28055
+ legacyReasons.push("pre-embeddings migration");
28056
+ }
28057
+ if (entity.memory_tier === "draft" && ageDays > 60 && !entity.promoted_from_id) {
28058
+ legacy = true;
28059
+ legacyReasons.push("stuck draft");
28060
+ }
28061
+ if (!hasTags && !hasRelations) {
28062
+ legacy = true;
28063
+ legacyReasons.push("no graph presence");
28064
+ }
28065
+ let bucket;
28066
+ if (score < deleteBelow)
28067
+ bucket = "delete";
28068
+ else if (score < archiveBelow)
28069
+ bucket = "archive";
28070
+ else if (score < 70)
28071
+ bucket = "review";
28072
+ else
28073
+ bucket = "keep";
28074
+ const staleDraft = entity.memory_tier === "draft" && (entity.access_count || 0) === 0 && ageDays > staleDraftAgeDays;
28075
+ return {
28076
+ id: entity.id,
28077
+ title: entity.title,
28078
+ type: entity.type,
28079
+ tier: entity.memory_tier,
28080
+ ageDays: Math.round(ageDays),
28081
+ score,
28082
+ bucket,
28083
+ reasons,
28084
+ legacy,
28085
+ legacyReasons,
28086
+ staleDraft,
28087
+ subScores: {
28088
+ confidence: Math.round(confidence),
28089
+ decay: Math.round(decay),
28090
+ structural,
28091
+ content,
28092
+ tierAgeFit,
28093
+ access: Math.round(access)
28094
+ }
28095
+ };
28096
+ }
28097
+ async function runMemoryAudit(client3, workspaceId, projectId, options) {
28098
+ const dryRun = options?.dryRun !== false;
28099
+ const archiveBelow = options?.archiveBelow ?? 40;
28100
+ const deleteBelow = options?.deleteBelow ?? 20;
28101
+ const limit = options?.limit ?? 500;
28102
+ const staleDraftAgeDays = options?.staleDraftAgeDays ?? 7;
28103
+ const report = {
28104
+ success: true,
28105
+ dryRun,
28106
+ timestamp: new Date().toISOString(),
28107
+ workspace: { id: workspaceId, projectId },
28108
+ summary: {
28109
+ totalEntities: 0,
28110
+ scanned: 0,
28111
+ keep: 0,
28112
+ review: 0,
28113
+ archive: 0,
28114
+ delete: 0,
28115
+ legacyCount: 0,
28116
+ staleDraftCount: 0
28117
+ },
28118
+ actionsTaken: { flaggedReview: 0, archived: 0, deleted: 0 },
28119
+ distribution: { "0-20": 0, "20-40": 0, "40-70": 0, "70-100": 0 },
28120
+ legacyBreakdown: {
28121
+ defaultConfidence: 0,
28122
+ missingEmbedding: 0,
28123
+ stuckDraft: 0,
28124
+ noGraphPresence: 0
28125
+ },
28126
+ lowest: [],
28127
+ staleDrafts: [],
28128
+ errors: [],
28129
+ healthReport: ""
28130
+ };
28131
+ const entities = [];
28132
+ let offset = 0;
28006
28133
  try {
28007
- if (session.memoryReadCount > 0) {
28008
- session.allActions.push({
28009
- action: `Recalled ${session.memoryReadCount} memor${session.memoryReadCount === 1 ? "y" : "ies"}`,
28010
- ts: new Date().toISOString()
28134
+ while (entities.length < limit) {
28135
+ const pageSize = Math.min(BATCH_SIZE, limit - entities.length);
28136
+ const result = await client3.listMemoryEntities({
28137
+ workspace_id: workspaceId,
28138
+ project_id: projectId,
28139
+ limit: pageSize,
28140
+ offset
28141
+ });
28142
+ const page = result.entities || [];
28143
+ if (page.length === 0)
28144
+ break;
28145
+ entities.push(...page);
28146
+ if (page.length < pageSize)
28147
+ break;
28148
+ offset += pageSize;
28149
+ }
28150
+ } catch (err) {
28151
+ report.errors.push({
28152
+ step: "fetch",
28153
+ message: `Failed to fetch entities: ${err.message}`
28154
+ });
28155
+ report.success = false;
28156
+ report.healthReport = renderReport(report);
28157
+ return report;
28158
+ }
28159
+ report.summary.totalEntities = entities.length;
28160
+ const relationCounts = new Map;
28161
+ for (let i = 0;i < entities.length; i += CONCURRENCY_LIMIT) {
28162
+ const batch = entities.slice(i, i + CONCURRENCY_LIMIT);
28163
+ const results = await Promise.allSettled(batch.map(async (e) => {
28164
+ const related = await client3.getRelatedEntities(e.id);
28165
+ const count = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
28166
+ return { id: e.id, count };
28167
+ }));
28168
+ for (const r of results) {
28169
+ if (r.status === "fulfilled") {
28170
+ relationCounts.set(r.value.id, r.value.count);
28171
+ }
28172
+ }
28173
+ }
28174
+ const audits = [];
28175
+ for (const entity of entities) {
28176
+ const relCount = relationCounts.get(entity.id) ?? 0;
28177
+ const audit = scoreEntity(entity, relCount, archiveBelow, deleteBelow, staleDraftAgeDays);
28178
+ audits.push(audit);
28179
+ report.summary.scanned++;
28180
+ report.summary[audit.bucket]++;
28181
+ if (audit.legacy)
28182
+ report.summary.legacyCount++;
28183
+ if (audit.staleDraft)
28184
+ report.summary.staleDraftCount++;
28185
+ if (audit.score < 20)
28186
+ report.distribution["0-20"]++;
28187
+ else if (audit.score < 40)
28188
+ report.distribution["20-40"]++;
28189
+ else if (audit.score < 70)
28190
+ report.distribution["40-70"]++;
28191
+ else
28192
+ report.distribution["70-100"]++;
28193
+ for (const reason of audit.legacyReasons) {
28194
+ if (reason.startsWith("default confidence"))
28195
+ report.legacyBreakdown.defaultConfidence++;
28196
+ else if (reason.startsWith("pre-embeddings"))
28197
+ report.legacyBreakdown.missingEmbedding++;
28198
+ else if (reason.startsWith("stuck draft"))
28199
+ report.legacyBreakdown.stuckDraft++;
28200
+ else if (reason.startsWith("no graph"))
28201
+ report.legacyBreakdown.noGraphPresence++;
28202
+ }
28203
+ }
28204
+ report.lowest = [...audits].sort((a, b) => a.score - b.score).slice(0, 10);
28205
+ report.staleDrafts = audits.filter((a) => a.staleDraft).sort((a, b) => b.ageDays - a.ageDays);
28206
+ if (!dryRun) {
28207
+ for (const audit of audits) {
28208
+ try {
28209
+ if (audit.bucket === "delete") {
28210
+ await client3.deleteMemoryEntity(audit.id);
28211
+ report.actionsTaken.deleted++;
28212
+ } else if (audit.bucket === "archive") {
28213
+ await client3.updateMemoryEntity(audit.id, {
28214
+ confidence: 0.25,
28215
+ metadata: {
28216
+ audit_archived_at: new Date().toISOString(),
28217
+ audit_score: audit.score,
28218
+ audit_reasons: audit.reasons
28219
+ }
28220
+ });
28221
+ report.actionsTaken.archived++;
28222
+ } else if (audit.bucket === "review") {
28223
+ await client3.updateMemoryEntity(audit.id, {
28224
+ metadata: {
28225
+ needs_review: true,
28226
+ audit_score: audit.score,
28227
+ audit_reasons: audit.reasons,
28228
+ audit_at: new Date().toISOString()
28229
+ }
28230
+ });
28231
+ report.actionsTaken.flaggedReview++;
28232
+ }
28233
+ } catch (err) {
28234
+ report.errors.push({
28235
+ entityId: audit.id,
28236
+ step: audit.bucket,
28237
+ message: err.message
28238
+ });
28239
+ }
28240
+ }
28241
+ }
28242
+ report.healthReport = renderReport(report);
28243
+ return report;
28244
+ }
28245
+ function renderReport(report) {
28246
+ const mode = report.dryRun ? "Dry Run (preview)" : "Executed";
28247
+ const s = report.summary;
28248
+ const lines = [
28249
+ `# Memory Quality Audit
28250
+ `,
28251
+ `**Mode:** ${mode} | **Scanned:** ${s.scanned}/${s.totalEntities} | **Legacy:** ${s.legacyCount} | **Stale drafts:** ${s.staleDraftCount}`,
28252
+ "",
28253
+ "## Distribution",
28254
+ `- 70-100 (keep): ${report.distribution["70-100"]}`,
28255
+ `- 40-69 (review): ${report.distribution["40-70"]}`,
28256
+ `- 20-39 (archive): ${report.distribution["20-40"]}`,
28257
+ `- 0-19 (delete): ${report.distribution["0-20"]}`,
28258
+ "",
28259
+ "## Buckets",
28260
+ `- **Keep:** ${s.keep}`,
28261
+ `- **Review:** ${s.review}${!report.dryRun ? ` (flagged ${report.actionsTaken.flaggedReview})` : ""}`,
28262
+ `- **Archive:** ${s.archive}${!report.dryRun ? ` (archived ${report.actionsTaken.archived})` : ""}`,
28263
+ `- **Delete:** ${s.delete}${!report.dryRun ? ` (deleted ${report.actionsTaken.deleted})` : ""}`,
28264
+ ""
28265
+ ];
28266
+ const l = report.legacyBreakdown;
28267
+ if (s.legacyCount > 0) {
28268
+ lines.push("## Legacy Breakdown");
28269
+ lines.push(`- Default confidence, never validated: ${l.defaultConfidence}`);
28270
+ lines.push(`- Pre-embeddings migration: ${l.missingEmbedding}`);
28271
+ lines.push(`- Stuck drafts (>60d, no promotion): ${l.stuckDraft}`);
28272
+ lines.push(`- No tags + no relations: ${l.noGraphPresence}`);
28273
+ lines.push("");
28274
+ }
28275
+ if (report.staleDrafts.length > 0) {
28276
+ lines.push("## Stale Drafts (promote-or-drop candidates)");
28277
+ lines.push("Filter: `tier=draft AND access=0 AND age>threshold`");
28278
+ lines.push("| Age | Score | Title |");
28279
+ lines.push("|-----|-------|-------|");
28280
+ for (const a of report.staleDrafts.slice(0, 20)) {
28281
+ const titleTrunc = a.title.length > 50 ? `${a.title.slice(0, 47)}...` : a.title;
28282
+ lines.push(`| ${a.ageDays}d | ${a.score} | ${titleTrunc} |`);
28283
+ }
28284
+ lines.push("");
28285
+ }
28286
+ if (report.lowest.length > 0) {
28287
+ lines.push("## Lowest-Scoring (top 10)");
28288
+ lines.push("| Score | Bucket | Tier | Age | Title | Reasons |");
28289
+ lines.push("|-------|--------|------|-----|-------|---------|");
28290
+ for (const a of report.lowest) {
28291
+ const reasonStr = a.reasons.slice(0, 3).join(", ") || "\u2014";
28292
+ const titleTrunc = a.title.length > 40 ? `${a.title.slice(0, 37)}...` : a.title;
28293
+ lines.push(`| ${a.score} | ${a.bucket} | ${a.tier} | ${a.ageDays}d | ${titleTrunc} | ${reasonStr} |`);
28294
+ }
28295
+ lines.push("");
28296
+ }
28297
+ if (report.errors.length > 0) {
28298
+ lines.push("## Errors");
28299
+ for (const e of report.errors.slice(0, 10)) {
28300
+ lines.push(`- **${e.step}${e.entityId ? ` ${e.entityId}` : ""}:** ${e.message}`);
28301
+ }
28302
+ lines.push("");
28303
+ }
28304
+ if (report.dryRun) {
28305
+ lines.push("---");
28306
+ lines.push("*Run with `dryRun: false` to flag review entries, archive low-quality memories, and delete worst offenders.*");
28307
+ }
28308
+ return lines.join(`
28309
+ `);
28310
+ }
28311
+
28312
+ // src/memory-cleanup.ts
28313
+ init_dist();
28314
+ var ALL_STEPS = [
28315
+ "prune",
28316
+ "consolidate",
28317
+ "orphans",
28318
+ "duplicates",
28319
+ "backfill",
28320
+ "audit"
28321
+ ];
28322
+ var MS_PER_DAY2 = 1000 * 60 * 60 * 24;
28323
+ var MAX_ENTITIES_FETCH = 200;
28324
+ var DUPLICATE_SIMILARITY_THRESHOLD = 0.85;
28325
+ var CONCURRENCY_LIMIT2 = 5;
28326
+ async function runMemoryCleanup(client3, workspaceId, projectId, options) {
28327
+ const dryRun = options?.dryRun !== false;
28328
+ const steps = options?.steps ?? ALL_STEPS;
28329
+ const maxAgeDays = options?.maxAgeDays ?? 30;
28330
+ const minClusterSize = options?.minClusterSize ?? 3;
28331
+ const orphanAgeDays = options?.orphanAgeDays ?? 14;
28332
+ const report = {
28333
+ success: true,
28334
+ dryRun,
28335
+ timestamp: new Date().toISOString(),
28336
+ workspace: { id: workspaceId, projectId },
28337
+ summary: { totalEntities: 0, issuesFound: 0, actionsTaken: 0 },
28338
+ steps: {},
28339
+ errors: [],
28340
+ healthReport: ""
28341
+ };
28342
+ let entities = [];
28343
+ try {
28344
+ const listResult = await client3.listMemoryEntities({
28345
+ workspace_id: workspaceId,
28346
+ project_id: projectId,
28347
+ limit: MAX_ENTITIES_FETCH
28348
+ });
28349
+ entities = listResult.entities || [];
28350
+ report.summary.totalEntities = entities.length;
28351
+ } catch (err) {
28352
+ report.errors.push({
28353
+ step: "init",
28354
+ message: `Failed to fetch entities: ${err.message}`
28355
+ });
28356
+ report.success = false;
28357
+ report.healthReport = generateHealthReport(report);
28358
+ return report;
28359
+ }
28360
+ if (steps.includes("prune")) {
28361
+ try {
28362
+ report.steps.prune = runPruneStep(entities, maxAgeDays);
28363
+ if (!dryRun) {
28364
+ for (const item of report.steps.prune.items) {
28365
+ try {
28366
+ await client3.deleteMemoryEntity(item.id);
28367
+ report.steps.prune.pruned++;
28368
+ } catch (err) {
28369
+ report.errors.push({
28370
+ step: "prune",
28371
+ message: `Failed to delete ${item.id}: ${err.message}`
28372
+ });
28373
+ }
28374
+ }
28375
+ report.summary.actionsTaken += report.steps.prune.pruned;
28376
+ }
28377
+ report.summary.issuesFound += report.steps.prune.staleDraftsFound;
28378
+ } catch (err) {
28379
+ report.errors.push({
28380
+ step: "prune",
28381
+ message: err.message
28382
+ });
28383
+ }
28384
+ }
28385
+ if (steps.includes("consolidate")) {
28386
+ try {
28387
+ const result = await consolidateMemories(client3, workspaceId, projectId, {
28388
+ dryRun,
28389
+ minClusterSize
28390
+ });
28391
+ report.steps.consolidate = {
28392
+ clustersFound: result.clustersFound,
28393
+ entitiesProcessed: result.entitiesProcessed,
28394
+ consolidated: result.consolidated,
28395
+ details: result.details
28396
+ };
28397
+ report.summary.issuesFound += result.clustersFound;
28398
+ if (!dryRun)
28399
+ report.summary.actionsTaken += result.consolidated;
28400
+ } catch (err) {
28401
+ report.errors.push({
28402
+ step: "consolidate",
28403
+ message: err.message
28404
+ });
28405
+ }
28406
+ }
28407
+ if (steps.includes("orphans")) {
28408
+ try {
28409
+ report.steps.orphans = await runOrphanStep(client3, entities, orphanAgeDays);
28410
+ if (!dryRun) {
28411
+ for (const item of report.steps.orphans.items) {
28412
+ try {
28413
+ await client3.deleteMemoryEntity(item.id);
28414
+ report.steps.orphans.removed++;
28415
+ } catch (err) {
28416
+ report.errors.push({
28417
+ step: "orphans",
28418
+ message: `Failed to delete ${item.id}: ${err.message}`
28419
+ });
28420
+ }
28421
+ }
28422
+ report.summary.actionsTaken += report.steps.orphans.removed;
28423
+ }
28424
+ report.summary.issuesFound += report.steps.orphans.orphansFound;
28425
+ } catch (err) {
28426
+ report.errors.push({
28427
+ step: "orphans",
28428
+ message: err.message
28429
+ });
28430
+ }
28431
+ }
28432
+ if (steps.includes("duplicates")) {
28433
+ try {
28434
+ report.steps.duplicates = await runDuplicateStep(client3, entities, workspaceId, projectId);
28435
+ if (!dryRun) {
28436
+ for (const pair of report.steps.duplicates.pairs) {
28437
+ try {
28438
+ await client3.deleteMemoryEntity(pair.removeId);
28439
+ report.steps.duplicates.resolved++;
28440
+ } catch (err) {
28441
+ report.errors.push({
28442
+ step: "duplicates",
28443
+ message: `Failed to delete ${pair.removeId}: ${err.message}`
28444
+ });
28445
+ }
28446
+ }
28447
+ report.summary.actionsTaken += report.steps.duplicates.resolved;
28448
+ }
28449
+ report.summary.issuesFound += report.steps.duplicates.duplicatePairsFound;
28450
+ } catch (err) {
28451
+ report.errors.push({
28452
+ step: "duplicates",
28453
+ message: err.message
28454
+ });
28455
+ }
28456
+ }
28457
+ if (steps.includes("backfill")) {
28458
+ try {
28459
+ if (dryRun) {
28460
+ report.steps.backfill = {
28461
+ processed: 0,
28462
+ remaining: -1,
28463
+ errors: []
28464
+ };
28465
+ } else {
28466
+ const result = await client3.backfillEmbeddings(workspaceId);
28467
+ report.steps.backfill = {
28468
+ processed: result.processed,
28469
+ remaining: result.remaining,
28470
+ errors: result.errors || []
28471
+ };
28472
+ report.summary.actionsTaken += result.processed;
28473
+ }
28474
+ } catch (err) {
28475
+ report.errors.push({
28476
+ step: "backfill",
28477
+ message: err.message
28478
+ });
28479
+ }
28480
+ }
28481
+ if (steps.includes("audit")) {
28482
+ try {
28483
+ const auditReport = await runMemoryAudit(client3, workspaceId, projectId, {
28484
+ dryRun,
28485
+ archiveBelow: options?.auditArchiveBelow,
28486
+ deleteBelow: options?.auditDeleteBelow
28487
+ });
28488
+ const low = auditReport.lowest.length > 0 ? auditReport.lowest[0].score : null;
28489
+ report.steps.audit = {
28490
+ scanned: auditReport.summary.scanned,
28491
+ legacyCount: auditReport.summary.legacyCount,
28492
+ buckets: {
28493
+ keep: auditReport.summary.keep,
28494
+ review: auditReport.summary.review,
28495
+ archive: auditReport.summary.archive,
28496
+ delete: auditReport.summary.delete
28497
+ },
28498
+ actions: auditReport.actionsTaken,
28499
+ lowestScore: low,
28500
+ report: auditReport
28501
+ };
28502
+ report.summary.issuesFound += auditReport.summary.review + auditReport.summary.archive + auditReport.summary.delete;
28503
+ if (!dryRun) {
28504
+ report.summary.actionsTaken += auditReport.actionsTaken.flaggedReview + auditReport.actionsTaken.archived + auditReport.actionsTaken.deleted;
28505
+ }
28506
+ for (const err of auditReport.errors) {
28507
+ report.errors.push({
28508
+ step: `audit:${err.step}`,
28509
+ message: err.entityId ? `${err.entityId}: ${err.message}` : err.message
28510
+ });
28511
+ }
28512
+ } catch (err) {
28513
+ report.errors.push({
28514
+ step: "audit",
28515
+ message: err.message
28516
+ });
28517
+ }
28518
+ }
28519
+ report.healthReport = generateHealthReport(report);
28520
+ return report;
28521
+ }
28522
+ function runPruneStep(entities, maxAgeDays) {
28523
+ const now = Date.now();
28524
+ const drafts = entities.filter((e) => e.memory_tier === "draft");
28525
+ const stale = [];
28526
+ for (const entity of drafts) {
28527
+ const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY2;
28528
+ if (ageDays < maxAgeDays)
28529
+ continue;
28530
+ const lifecycle2 = evaluateLifecycle(entity);
28531
+ stale.push({
28532
+ id: entity.id,
28533
+ title: entity.title,
28534
+ ageDays: Math.round(ageDays),
28535
+ decayScore: Math.round(lifecycle2.decay.score * 100) / 100
28536
+ });
28537
+ }
28538
+ return { staleDraftsFound: stale.length, pruned: 0, items: stale };
28539
+ }
28540
+ async function runOrphanStep(client3, entities, orphanAgeDays) {
28541
+ const now = Date.now();
28542
+ const result = { orphansFound: 0, removed: 0, items: [] };
28543
+ const candidates = entities.filter((e) => {
28544
+ if (e.memory_tier === "reference")
28545
+ return false;
28546
+ if (e.access_count >= 2)
28547
+ return false;
28548
+ const ageDays = (now - new Date(e.created_at).getTime()) / MS_PER_DAY2;
28549
+ return ageDays >= orphanAgeDays;
28550
+ });
28551
+ for (let i = 0;i < candidates.length; i += CONCURRENCY_LIMIT2) {
28552
+ const batch = candidates.slice(i, i + CONCURRENCY_LIMIT2);
28553
+ const results = await Promise.allSettled(batch.map(async (entity) => {
28554
+ const related = await client3.getRelatedEntities(entity.id);
28555
+ const totalRelations = (related.outgoing?.length || 0) + (related.incoming?.length || 0);
28556
+ if (totalRelations > 0)
28557
+ return null;
28558
+ const ageDays = (now - new Date(entity.created_at).getTime()) / MS_PER_DAY2;
28559
+ return {
28560
+ id: entity.id,
28561
+ title: entity.title,
28562
+ type: entity.type,
28563
+ tier: entity.memory_tier,
28564
+ ageDays: Math.round(ageDays),
28565
+ accessCount: entity.access_count
28566
+ };
28567
+ }));
28568
+ for (const r of results) {
28569
+ if (r.status === "fulfilled" && r.value) {
28570
+ result.items.push(r.value);
28571
+ result.orphansFound++;
28572
+ }
28573
+ }
28574
+ }
28575
+ return result;
28576
+ }
28577
+ async function runDuplicateStep(client3, entities, workspaceId, projectId) {
28578
+ const result = {
28579
+ duplicatePairsFound: 0,
28580
+ resolved: 0,
28581
+ pairs: []
28582
+ };
28583
+ const seenPairs = new Set;
28584
+ const flaggedForRemoval = new Set;
28585
+ const entityMap = new Map(entities.map((e) => [e.id, e]));
28586
+ const similarityMap = new Map;
28587
+ for (let i = 0;i < entities.length; i += CONCURRENCY_LIMIT2) {
28588
+ const batch = entities.slice(i, i + CONCURRENCY_LIMIT2);
28589
+ const results = await Promise.allSettled(batch.map(async (entity) => {
28590
+ const similar = await findSimilarEntities(client3, entity.title, entity.content, workspaceId, { projectId, limit: 5, minRrfScore: 0.05, excludeIds: [entity.id] });
28591
+ return { entityId: entity.id, similar };
28592
+ }));
28593
+ for (const r of results) {
28594
+ if (r.status === "fulfilled") {
28595
+ similarityMap.set(r.value.entityId, r.value.similar);
28596
+ }
28597
+ }
28598
+ }
28599
+ for (const entity of entities) {
28600
+ if (flaggedForRemoval.has(entity.id))
28601
+ continue;
28602
+ const similar = similarityMap.get(entity.id) || [];
28603
+ for (const match2 of similar) {
28604
+ if (flaggedForRemoval.has(match2.id))
28605
+ continue;
28606
+ const pairKey = [entity.id, match2.id].sort().join(":");
28607
+ if (seenPairs.has(pairKey))
28608
+ continue;
28609
+ seenPairs.add(pairKey);
28610
+ const sim = titleSimilarity(entity.title, match2.title);
28611
+ if (sim < DUPLICATE_SIMILARITY_THRESHOLD)
28612
+ continue;
28613
+ const entityScore = entityQualityScore(entity);
28614
+ const matchEntity = entityMap.get(match2.id);
28615
+ const matchScore = matchEntity ? entityQualityScore(matchEntity) : match2.confidence;
28616
+ const [keep, remove] = entityScore >= matchScore ? [entity, { id: match2.id, title: match2.title }] : [{ id: match2.id, title: match2.title }, entity];
28617
+ flaggedForRemoval.add(remove.id);
28618
+ result.pairs.push({
28619
+ keepId: keep.id,
28620
+ keepTitle: keep.title,
28621
+ removeId: remove.id,
28622
+ removeTitle: remove.title,
28623
+ similarity: Math.round(sim * 100) / 100
28624
+ });
28625
+ result.duplicatePairsFound++;
28626
+ }
28627
+ }
28628
+ return result;
28629
+ }
28630
+ var TIER_WEIGHTS2 = {
28631
+ reference: 3,
28632
+ episode: 2,
28633
+ draft: 1
28634
+ };
28635
+ function entityQualityScore(entity) {
28636
+ return entity.confidence + (TIER_WEIGHTS2[entity.memory_tier] || 0) + Math.min(entity.access_count, 10) * 0.1;
28637
+ }
28638
+ function titleSimilarity(a, b) {
28639
+ const na = a.toLowerCase().trim();
28640
+ const nb = b.toLowerCase().trim();
28641
+ if (na === nb)
28642
+ return 1;
28643
+ const wordsA = new Set(na.split(/\W+/).filter(Boolean));
28644
+ const wordsB = new Set(nb.split(/\W+/).filter(Boolean));
28645
+ if (wordsA.size === 0 || wordsB.size === 0)
28646
+ return 0;
28647
+ let intersection3 = 0;
28648
+ for (const w of wordsA) {
28649
+ if (wordsB.has(w))
28650
+ intersection3++;
28651
+ }
28652
+ const union3 = wordsA.size + wordsB.size - intersection3;
28653
+ return union3 > 0 ? intersection3 / union3 : 0;
28654
+ }
28655
+ function generateHealthReport(report) {
28656
+ const mode = report.dryRun ? "Dry Run (preview)" : "Executed";
28657
+ const lines = [
28658
+ `# Memory Health Report
28659
+ `,
28660
+ `**Mode:** ${mode} | **Entities:** ${report.summary.totalEntities} | **Issues:** ${report.summary.issuesFound} | **Actions:** ${report.summary.actionsTaken}`,
28661
+ ""
28662
+ ];
28663
+ if (report.summary.totalEntities >= MAX_ENTITIES_FETCH) {
28664
+ lines.push(`> **Note:** Entity count hit the ${MAX_ENTITIES_FETCH} fetch limit. Some entities may not have been analyzed.
28665
+ `);
28666
+ }
28667
+ if (report.steps.prune) {
28668
+ const p = report.steps.prune;
28669
+ lines.push("## Stale Drafts");
28670
+ if (p.staleDraftsFound === 0) {
28671
+ lines.push(`No stale drafts found.
28672
+ `);
28673
+ } else {
28674
+ lines.push(`Found **${p.staleDraftsFound}** stale drafts${!report.dryRun ? ` (pruned ${p.pruned})` : ""}:`);
28675
+ lines.push("| Title | Age | Decay |");
28676
+ lines.push("|-------|-----|-------|");
28677
+ for (const item of p.items.slice(0, 20)) {
28678
+ lines.push(`| ${item.title} | ${item.ageDays}d | ${item.decayScore} |`);
28679
+ }
28680
+ lines.push("");
28681
+ }
28682
+ }
28683
+ if (report.steps.consolidate) {
28684
+ const c = report.steps.consolidate;
28685
+ lines.push("## Consolidation");
28686
+ if (c.clustersFound === 0) {
28687
+ lines.push(`Scanned ${c.entitiesProcessed} draft/episode entities \u2014 no clusters found.
28688
+ `);
28689
+ } else {
28690
+ lines.push(`Found **${c.clustersFound}** clusters across ${c.entitiesProcessed} entities:`);
28691
+ for (const d of c.details.slice(0, 10)) {
28692
+ lines.push(`- **${d.mergedTitle}** \u2014 ${d.clusterSize} entities`);
28693
+ }
28694
+ lines.push("");
28695
+ }
28696
+ }
28697
+ if (report.steps.orphans) {
28698
+ const o = report.steps.orphans;
28699
+ lines.push("## Orphaned Entities");
28700
+ if (o.orphansFound === 0) {
28701
+ lines.push(`No orphans found.
28702
+ `);
28703
+ } else {
28704
+ lines.push(`Found **${o.orphansFound}** orphans${!report.dryRun ? ` (removed ${o.removed})` : ""}:`);
28705
+ lines.push("| Title | Type | Tier | Age | Accesses |");
28706
+ lines.push("|-------|------|------|-----|----------|");
28707
+ for (const item of o.items.slice(0, 20)) {
28708
+ lines.push(`| ${item.title} | ${item.type} | ${item.tier} | ${item.ageDays}d | ${item.accessCount} |`);
28709
+ }
28710
+ lines.push("");
28711
+ }
28712
+ }
28713
+ if (report.steps.duplicates) {
28714
+ const d = report.steps.duplicates;
28715
+ lines.push("## Near-Duplicates");
28716
+ if (d.duplicatePairsFound === 0) {
28717
+ lines.push(`No duplicates found.
28718
+ `);
28719
+ } else {
28720
+ lines.push(`Found **${d.duplicatePairsFound}** duplicate pairs${!report.dryRun ? ` (resolved ${d.resolved})` : ""}:`);
28721
+ for (const pair of d.pairs.slice(0, 20)) {
28722
+ lines.push(`- "${pair.keepTitle}" ~ "${pair.removeTitle}" (${Math.round(pair.similarity * 100)}% similar, keep first)`);
28723
+ }
28724
+ lines.push("");
28725
+ }
28726
+ }
28727
+ if (report.steps.backfill) {
28728
+ const b = report.steps.backfill;
28729
+ lines.push("## Embedding Coverage");
28730
+ if (report.dryRun) {
28731
+ lines.push("Backfill will run when executed with `dryRun: false`.\n");
28732
+ } else if (b.remaining === 0) {
28733
+ lines.push(`All embeddings up to date (processed ${b.processed}).
28734
+ `);
28735
+ } else {
28736
+ lines.push(`Processed ${b.processed} entities. ${b.remaining} still need embeddings.
28737
+ `);
28738
+ }
28739
+ }
28740
+ if (report.steps.audit) {
28741
+ const a = report.steps.audit;
28742
+ lines.push("## Quality Audit");
28743
+ lines.push(`Scanned ${a.scanned} entities. Legacy signals on ${a.legacyCount}.`);
28744
+ lines.push(`Buckets \u2014 keep: ${a.buckets.keep}, review: ${a.buckets.review}, archive: ${a.buckets.archive}, delete: ${a.buckets.delete}.`);
28745
+ if (!report.dryRun) {
28746
+ lines.push(`Actions \u2014 flagged: ${a.actions.flaggedReview}, archived: ${a.actions.archived}, deleted: ${a.actions.deleted}.`);
28747
+ }
28748
+ if (a.report.lowest.length > 0) {
28749
+ const worst = a.report.lowest[0];
28750
+ lines.push(`Lowest score: **${worst.score}** \u2014 "${worst.title}" (${worst.reasons.slice(0, 2).join(", ") || "\u2014"}).`);
28751
+ }
28752
+ lines.push("");
28753
+ }
28754
+ if (report.errors.length > 0) {
28755
+ lines.push("## Errors");
28756
+ for (const e of report.errors) {
28757
+ lines.push(`- **${e.step}:** ${e.message}`);
28758
+ }
28759
+ lines.push("");
28760
+ }
28761
+ if (report.dryRun) {
28762
+ lines.push("---\n*Run with `dryRun: false` to execute cleanup.*");
28763
+ }
28764
+ return lines.join(`
28765
+ `);
28766
+ }
28767
+ async function purgeMemories(client3, workspaceId, projectId, options) {
28768
+ const dryRun = options.dryRun !== false;
28769
+ const { filters } = options;
28770
+ const hasFilter = filters.tier || filters.scope || filters.type || filters.olderThanDays !== undefined || filters.maxConfidence !== undefined || filters.tags && filters.tags.length > 0;
28771
+ if (!hasFilter) {
28772
+ throw new Error("At least one narrowing filter (tier, scope, type, olderThanDays, maxConfidence, tags) is required.");
28773
+ }
28774
+ const allMatches = [];
28775
+ let offset = 0;
28776
+ const pageSize = 100;
28777
+ const now = Date.now();
28778
+ while (true) {
28779
+ const result = await client3.listMemoryEntities({
28780
+ workspace_id: workspaceId,
28781
+ project_id: projectId,
28782
+ type: filters.type,
28783
+ scope: filters.scope,
28784
+ tags: filters.tags,
28785
+ limit: pageSize,
28786
+ offset
28787
+ });
28788
+ const entities = result.entities || [];
28789
+ if (entities.length === 0)
28790
+ break;
28791
+ for (const entity of entities) {
28792
+ if (filters.tier && entity.memory_tier !== filters.tier)
28793
+ continue;
28794
+ if (filters.maxConfidence !== undefined && entity.confidence > filters.maxConfidence)
28795
+ continue;
28796
+ if (filters.olderThanDays !== undefined) {
28797
+ const ref = entity.last_accessed_at || entity.created_at;
28798
+ const ageDays = (now - new Date(ref).getTime()) / MS_PER_DAY2;
28799
+ if (ageDays < filters.olderThanDays)
28800
+ continue;
28801
+ }
28802
+ allMatches.push(entity);
28803
+ }
28804
+ if (entities.length < pageSize)
28805
+ break;
28806
+ offset += pageSize;
28807
+ }
28808
+ const items = allMatches.map((e) => ({
28809
+ id: e.id,
28810
+ title: e.title,
28811
+ type: e.type,
28812
+ tier: e.memory_tier,
28813
+ confidence: e.confidence,
28814
+ ageDays: Math.round((now - new Date(e.last_accessed_at || e.created_at).getTime()) / MS_PER_DAY2)
28815
+ }));
28816
+ const errors3 = [];
28817
+ let purged = 0;
28818
+ if (!dryRun) {
28819
+ for (let i = 0;i < allMatches.length; i += CONCURRENCY_LIMIT2) {
28820
+ const batch = allMatches.slice(i, i + CONCURRENCY_LIMIT2);
28821
+ const results = await Promise.allSettled(batch.map((e) => client3.deleteMemoryEntity(e.id)));
28822
+ for (let j = 0;j < results.length; j++) {
28823
+ if (results[j].status === "fulfilled") {
28824
+ purged++;
28825
+ } else {
28826
+ errors3.push({
28827
+ entityId: batch[j].id,
28828
+ message: results[j].status === "rejected" ? String(results[j].reason) : "Unknown error"
28829
+ });
28830
+ }
28831
+ }
28832
+ }
28833
+ }
28834
+ return {
28835
+ success: errors3.length === 0,
28836
+ dryRun,
28837
+ timestamp: new Date().toISOString(),
28838
+ workspace: { id: workspaceId, projectId },
28839
+ filters,
28840
+ matched: allMatches.length,
28841
+ purged: dryRun ? 0 : purged,
28842
+ items,
28843
+ errors: errors3
28844
+ };
28845
+ }
28846
+
28847
+ // src/onboard.ts
28848
+ async function onboardNewUser(params) {
28849
+ const {
28850
+ email: email3,
28851
+ password,
28852
+ fullName,
28853
+ workspaceName = `${fullName}'s Workspace`,
28854
+ projectName = "My First Board",
28855
+ template = "kanban",
28856
+ keyName = "mcp-agent",
28857
+ apiUrl = getApiUrl()
28858
+ } = params;
28859
+ const signupResult = await signupUser(apiUrl, {
28860
+ email: email3,
28861
+ password,
28862
+ full_name: fullName
28863
+ });
28864
+ const token = signupResult.session.access_token;
28865
+ const workspaceResult = await requestWithBearer(apiUrl, token, "POST", "/workspaces", {
28866
+ name: workspaceName
28867
+ });
28868
+ const projectResult = await requestWithBearer(apiUrl, token, "POST", "/projects", {
28869
+ workspaceId: workspaceResult.workspace.id,
28870
+ name: projectName,
28871
+ template
28872
+ });
28873
+ const keyResult = await requestWithBearer(apiUrl, token, "POST", "/api-keys", {
28874
+ name: keyName
28875
+ });
28876
+ return {
28877
+ user: signupResult.user,
28878
+ workspace: workspaceResult.workspace,
28879
+ project: projectResult.project,
28880
+ columns: projectResult.columns,
28881
+ apiKey: {
28882
+ rawKey: keyResult.rawKey,
28883
+ prefix: keyResult.apiKey.prefix
28884
+ }
28885
+ };
28886
+ }
28887
+
28888
+ // src/server.ts
28889
+ var memorySessions = new Map;
28890
+ function parseLabelList(raw2) {
28891
+ if (raw2 === undefined || raw2 === null)
28892
+ return;
28893
+ if (Array.isArray(raw2)) {
28894
+ const arr = raw2.filter((v) => typeof v === "string").map((v) => v.trim()).filter((v) => v.length > 0);
28895
+ return arr.length ? arr : undefined;
28896
+ }
28897
+ if (typeof raw2 === "string") {
28898
+ const trimmed = raw2.trim();
28899
+ if (!trimmed)
28900
+ return;
28901
+ if (trimmed.startsWith("[")) {
28902
+ try {
28903
+ const parsed = JSON.parse(trimmed);
28904
+ if (Array.isArray(parsed) && parsed.every((x) => typeof x === "string")) {
28905
+ const arr = parsed.map((v) => v.trim()).filter((v) => v.length > 0);
28906
+ return arr.length ? arr : undefined;
28907
+ }
28908
+ } catch {}
28909
+ }
28910
+ return [trimmed];
28911
+ }
28912
+ return;
28913
+ }
28914
+ function initMemorySession(cardId, agentIdentifier, agentName) {
28915
+ memorySessions.set(cardId, {
28916
+ cardId,
28917
+ agentIdentifier,
28918
+ agentName,
28919
+ memoryReadCount: 0,
28920
+ pendingActions: [],
28921
+ allActions: [],
28922
+ dirty: false
28923
+ });
28924
+ }
28925
+ function getMemorySession(cardId) {
28926
+ return memorySessions.get(cardId);
28927
+ }
28928
+ function appendMemoryAction(cardId, action) {
28929
+ const session = memorySessions.get(cardId);
28930
+ if (!session)
28931
+ return;
28932
+ const truncated = action.length > 512 ? action.slice(0, 509) + "..." : action;
28933
+ const entry = { action: truncated, ts: new Date().toISOString() };
28934
+ session.pendingActions.push(entry);
28935
+ session.dirty = true;
28936
+ }
28937
+ function incrementMemoryReads(cardId) {
28938
+ const session = memorySessions.get(cardId);
28939
+ if (!session)
28940
+ return;
28941
+ session.memoryReadCount++;
28942
+ session.dirty = true;
28943
+ }
28944
+ async function flushMemoryActions(client3, cardId) {
28945
+ const session = memorySessions.get(cardId);
28946
+ if (!session || !session.dirty)
28947
+ return;
28948
+ try {
28949
+ if (session.memoryReadCount > 0) {
28950
+ session.allActions.push({
28951
+ action: `Recalled ${session.memoryReadCount} memor${session.memoryReadCount === 1 ? "y" : "ies"}`,
28952
+ ts: new Date().toISOString()
28011
28953
  });
28012
28954
  session.memoryReadCount = 0;
28013
28955
  }
@@ -28101,18 +29043,22 @@ var TOOLS = {
28101
29043
  }
28102
29044
  },
28103
29045
  harmony_move_card: {
28104
- description: "Move a card to a different column or position",
29046
+ description: "Move a card to a different column or position. Provide either columnId (UUID) or columnName (e.g. 'Review', 'Done').",
28105
29047
  inputSchema: {
28106
29048
  type: "object",
28107
29049
  properties: {
28108
29050
  cardId: { type: "string", description: "Card ID to move" },
28109
- columnId: { type: "string", description: "Target column ID" },
29051
+ columnId: { type: "string", description: "Target column ID (UUID)" },
29052
+ columnName: {
29053
+ type: "string",
29054
+ description: "Target column name (e.g. 'To Do', 'In Progress', 'Review', 'Done'). Used if columnId is not provided."
29055
+ },
28110
29056
  position: {
28111
29057
  type: "number",
28112
29058
  description: "Position in column (0-indexed)"
28113
29059
  }
28114
29060
  },
28115
- required: ["cardId", "columnId"]
29061
+ required: ["cardId"]
28116
29062
  }
28117
29063
  },
28118
29064
  harmony_archive_card: {
@@ -28242,6 +29188,16 @@ var TOOLS = {
28242
29188
  required: ["name", "color"]
28243
29189
  }
28244
29190
  },
29191
+ harmony_delete_label: {
29192
+ description: "Delete a label from a project. Also removes it from any cards that reference it.",
29193
+ inputSchema: {
29194
+ type: "object",
29195
+ properties: {
29196
+ labelId: { type: "string", description: "Label ID to delete" }
29197
+ },
29198
+ required: ["labelId"]
29199
+ }
29200
+ },
28245
29201
  harmony_add_label_to_card: {
28246
29202
  description: "Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
28247
29203
  inputSchema: {
@@ -28516,6 +29472,20 @@ var TOOLS = {
28516
29472
  estimatedMinutesRemaining: {
28517
29473
  type: "number",
28518
29474
  description: "Updated time estimate"
29475
+ },
29476
+ actions: {
29477
+ type: "array",
29478
+ items: {
29479
+ type: "object",
29480
+ properties: {
29481
+ description: {
29482
+ type: "string",
29483
+ description: "What was done, e.g. 'Edited CardDetailSheet.tsx \u2014 added done toggle'"
29484
+ }
29485
+ },
29486
+ required: ["description"]
29487
+ },
29488
+ description: "Actions performed since last update. Each becomes a visible activity log entry."
28519
29489
  }
28520
29490
  },
28521
29491
  required: ["cardId", "agentIdentifier", "agentName"]
@@ -29356,6 +30326,146 @@ var TOOLS = {
29356
30326
  },
29357
30327
  required: []
29358
30328
  }
30329
+ },
30330
+ harmony_cleanup_memories: {
30331
+ description: "Run a unified memory cleanup: prune stale drafts, consolidate similar memories, detect orphans and duplicates, backfill embeddings, and optionally run a quality audit. Returns a health report. Dry-run by default \u2014 run with dryRun=false to execute.",
30332
+ inputSchema: {
30333
+ type: "object",
30334
+ properties: {
30335
+ workspaceId: {
30336
+ type: "string",
30337
+ description: "Workspace ID (optional if context set)"
30338
+ },
30339
+ projectId: {
30340
+ type: "string",
30341
+ description: "Project ID (optional)"
30342
+ },
30343
+ dryRun: {
30344
+ type: "boolean",
30345
+ description: "Preview cleanup without executing changes (default: true)"
30346
+ },
30347
+ steps: {
30348
+ type: "array",
30349
+ items: {
30350
+ type: "string",
30351
+ enum: [
30352
+ "prune",
30353
+ "consolidate",
30354
+ "orphans",
30355
+ "duplicates",
30356
+ "backfill",
30357
+ "audit"
30358
+ ]
30359
+ },
30360
+ description: "Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill, audit."
30361
+ },
30362
+ maxAgeDays: {
30363
+ type: "number",
30364
+ description: "Max age in days for stale draft pruning (default: 30)"
30365
+ },
30366
+ minClusterSize: {
30367
+ type: "number",
30368
+ description: "Min cluster size for consolidation (default: 3)"
30369
+ },
30370
+ orphanAgeDays: {
30371
+ type: "number",
30372
+ description: "Min age in days for orphan detection (default: 14)"
30373
+ },
30374
+ auditArchiveBelow: {
30375
+ type: "number",
30376
+ description: "Audit: archive entities scoring below this (default: 40)"
30377
+ },
30378
+ auditDeleteBelow: {
30379
+ type: "number",
30380
+ description: "Audit: delete entities scoring below this (default: 20)"
30381
+ }
30382
+ },
30383
+ required: []
30384
+ }
30385
+ },
30386
+ harmony_audit_memories: {
30387
+ description: "Rate every memory against state-of-the-art quality standards (confidence, decay, structural completeness, content, tier-age fit, access). Flags legacy entities from before recent optimizations (default confidence, missing embeddings, stuck drafts). Buckets: keep (\u226570), review (40-69), archive (20-39), delete (<20). Dry-run by default.",
30388
+ inputSchema: {
30389
+ type: "object",
30390
+ properties: {
30391
+ workspaceId: {
30392
+ type: "string",
30393
+ description: "Workspace ID (optional if context set)"
30394
+ },
30395
+ projectId: {
30396
+ type: "string",
30397
+ description: "Project ID (optional)"
30398
+ },
30399
+ dryRun: {
30400
+ type: "boolean",
30401
+ description: "Preview audit without flagging/archiving/deleting (default: true)"
30402
+ },
30403
+ archiveBelow: {
30404
+ type: "number",
30405
+ description: "Score threshold below which entities are archived (confidence set to 0.25). Default: 40"
30406
+ },
30407
+ deleteBelow: {
30408
+ type: "number",
30409
+ description: "Score threshold below which entities are hard-deleted. Default: 20. Set to 0 to never delete."
30410
+ },
30411
+ limit: {
30412
+ type: "number",
30413
+ description: "Max number of entities to audit (default: 500). Paginated fetch."
30414
+ },
30415
+ staleDraftAgeDays: {
30416
+ type: "number",
30417
+ description: "Age threshold (days) for the stale-draft filter: flags drafts with 0 accesses older than this. Reported separately from bucket scoring \u2014 surfaces promote-or-drop candidates the thresholds miss. Default: 7."
30418
+ }
30419
+ },
30420
+ required: []
30421
+ }
30422
+ },
30423
+ harmony_purge_memories: {
30424
+ description: "Bulk-delete memory entities matching filters within a project. Requires at least one narrowing filter (tier, scope, type, olderThanDays, maxConfidence, tags). Dry-run by default \u2014 preview what would be deleted before executing.",
30425
+ inputSchema: {
30426
+ type: "object",
30427
+ properties: {
30428
+ workspaceId: {
30429
+ type: "string",
30430
+ description: "Workspace ID (optional if context set)"
30431
+ },
30432
+ projectId: {
30433
+ type: "string",
30434
+ description: "Project ID (required \u2014 purge is project-scoped). Falls back to active project context."
30435
+ },
30436
+ dryRun: {
30437
+ type: "boolean",
30438
+ description: "Preview what would be deleted without executing (default: true)"
30439
+ },
30440
+ tier: {
30441
+ type: "string",
30442
+ enum: ["draft", "episode", "reference"],
30443
+ description: 'Filter by memory tier (e.g. "draft")'
30444
+ },
30445
+ scope: {
30446
+ type: "string",
30447
+ description: 'Filter by scope (e.g. "private", "project", "workspace")'
30448
+ },
30449
+ type: {
30450
+ type: "string",
30451
+ description: 'Filter by entity type (e.g. "error", "pattern", "lesson", "decision")'
30452
+ },
30453
+ olderThanDays: {
30454
+ type: "number",
30455
+ description: "Only include entities not accessed in at least this many days"
30456
+ },
30457
+ maxConfidence: {
30458
+ type: "number",
30459
+ description: "Only include entities with confidence at or below this value (e.g. 0.3 for low-confidence junk)"
30460
+ },
30461
+ tags: {
30462
+ type: "array",
30463
+ items: { type: "string" },
30464
+ description: "Only include entities matching these tags"
30465
+ }
30466
+ },
30467
+ required: []
30468
+ }
29359
30469
  }
29360
30470
  };
29361
30471
  var RESOURCES = [
@@ -29574,6 +30684,17 @@ function registerHandlers(server, deps) {
29574
30684
  throw new Error(`Unknown resource: ${uri}`);
29575
30685
  });
29576
30686
  }
30687
+ async function resolveColumnByName(client3, projectId, columnName) {
30688
+ const board = await client3.getBoard(projectId, { summary: true });
30689
+ const columns = board.columns;
30690
+ const lower = columnName.toLowerCase();
30691
+ const col = columns.find((c) => c.name.toLowerCase() === lower) || columns.find((c) => c.name.toLowerCase().includes(lower));
30692
+ if (!col) {
30693
+ const available = columns.map((c) => c.name).join(", ");
30694
+ throw new Error(`Column "${columnName}" not found. Available columns: ${available}`);
30695
+ }
30696
+ return col;
30697
+ }
29577
30698
  async function handleToolCall(name, args, deps) {
29578
30699
  const unauthenticatedTools = ["harmony_signup", "harmony_onboard"];
29579
30700
  if (!unauthenticatedTools.includes(name) && !deps.isConfigured()) {
@@ -29623,16 +30744,29 @@ async function handleToolCall(name, args, deps) {
29623
30744
  }
29624
30745
  case "harmony_move_card": {
29625
30746
  const cardId = exports_external.string().uuid().parse(args.cardId);
29626
- const columnId = exports_external.string().uuid().parse(args.columnId);
29627
30747
  const position = args.position !== undefined ? exports_external.number().int().min(0).parse(args.position) : undefined;
30748
+ let columnId;
30749
+ let resolvedProjectId;
30750
+ if (args.columnId) {
30751
+ columnId = exports_external.string().uuid().parse(args.columnId);
30752
+ } else if (args.columnName) {
30753
+ const columnName = exports_external.string().parse(args.columnName);
30754
+ const { card: cardForProject } = await client3.getCard(cardId);
30755
+ resolvedProjectId = cardForProject?.project_id;
30756
+ if (!resolvedProjectId)
30757
+ throw new Error("Card has no project");
30758
+ const col = await resolveColumnByName(client3, resolvedProjectId, columnName);
30759
+ columnId = col.id;
30760
+ } else {
30761
+ throw new Error("Either columnId or columnName is required");
30762
+ }
29628
30763
  const result = await client3.moveCard(cardId, columnId, position);
29629
30764
  let sessionEnded = false;
29630
30765
  try {
29631
30766
  const { card } = result;
29632
- if (card?.project_id) {
29633
- const board = await client3.getBoard(card.project_id, {
29634
- summary: true
29635
- });
30767
+ const projectId = card?.project_id || resolvedProjectId;
30768
+ if (projectId) {
30769
+ const board = await client3.getBoard(projectId, { summary: true });
29636
30770
  const columns = board.columns;
29637
30771
  const destCol = columns.find((c) => c.id === columnId);
29638
30772
  const colName = destCol?.name?.toLowerCase() || "";
@@ -29717,6 +30851,11 @@ async function handleToolCall(name, args, deps) {
29717
30851
  const result = await client3.createLabel(projectId, { name: name2, color });
29718
30852
  return { success: true, ...result };
29719
30853
  }
30854
+ case "harmony_delete_label": {
30855
+ const labelId = exports_external.string().uuid().parse(args.labelId);
30856
+ const result = await client3.deleteLabel(labelId);
30857
+ return { success: true, ...result };
30858
+ }
29720
30859
  case "harmony_add_label_to_card": {
29721
30860
  const cardId = exports_external.string().uuid().parse(args.cardId);
29722
30861
  let labelId = args.labelId ? exports_external.string().uuid().parse(args.labelId) : undefined;
@@ -29852,7 +30991,7 @@ async function handleToolCall(name, args, deps) {
29852
30991
  const agentIdentifier = exports_external.string().min(1).max(100).parse(args.agentIdentifier);
29853
30992
  const agentName = exports_external.string().min(1).max(100).parse(args.agentName);
29854
30993
  const moveToColumn = args.moveToColumn;
29855
- const addLabels = args.addLabels;
30994
+ const addLabels = parseLabelList(args.addLabels);
29856
30995
  let movedTo = null;
29857
30996
  const labelsAdded = [];
29858
30997
  if (moveToColumn || addLabels?.length) {
@@ -29961,12 +31100,20 @@ async function handleToolCall(name, args, deps) {
29961
31100
  const agentIdentifier = exports_external.string().min(1).max(100).parse(args.agentIdentifier);
29962
31101
  const agentName = exports_external.string().min(1).max(100).parse(args.agentName);
29963
31102
  const progressPercent = args.progressPercent !== undefined ? exports_external.number().min(0).max(100).parse(args.progressPercent) : undefined;
29964
- const callerRecentActions = args.recentActions;
31103
+ const callerActions = args.actions;
31104
+ const now = new Date().toISOString();
31105
+ const callerRecentActions = [
31106
+ ...args.recentActions || [],
31107
+ ...(callerActions || []).map((a) => ({
31108
+ action: a.description,
31109
+ ts: now
31110
+ }))
31111
+ ];
29965
31112
  const memSession = getMemorySession(cardId);
29966
31113
  let mergedRecentActions;
29967
31114
  if (memSession?.dirty) {
29968
- mergedRecentActions = mergeMemoryActionsInto(cardId, callerRecentActions || []);
29969
- } else if (callerRecentActions) {
31115
+ mergedRecentActions = mergeMemoryActionsInto(cardId, callerRecentActions);
31116
+ } else if (callerRecentActions.length > 0) {
29970
31117
  mergedRecentActions = callerRecentActions;
29971
31118
  }
29972
31119
  const result = await client3.updateAgentProgress(cardId, {
@@ -30004,27 +31151,21 @@ async function handleToolCall(name, args, deps) {
30004
31151
  const endProgressPercent = args.progressPercent !== undefined ? exports_external.number().min(0).max(100).parse(args.progressPercent) : undefined;
30005
31152
  await flushMemoryActions(client3, cardId);
30006
31153
  cleanupMemorySession(cardId);
30007
- const result = await client3.endAgentSession(cardId, {
30008
- status: sessionStatus,
30009
- progressPercent: endProgressPercent
30010
- });
31154
+ let result = { session: null };
31155
+ let sessionEndError = null;
31156
+ try {
31157
+ result = await client3.endAgentSession(cardId, {
31158
+ status: sessionStatus,
31159
+ progressPercent: endProgressPercent
31160
+ });
31161
+ } catch (err) {
31162
+ sessionEndError = err instanceof Error ? err.message : "Failed to end session";
31163
+ }
30011
31164
  untrack(cardId);
30012
31165
  let movedTo = null;
30013
- const _learningsExtracted = 0;
30014
- let _cardTitle = "";
30015
- let _cardLabels = [];
30016
- let _cardDescription = "";
30017
- let _cardSubtasks = [];
30018
31166
  try {
30019
31167
  const { card } = await client3.getCard(cardId);
30020
31168
  const typedCard = card;
30021
- _cardTitle = typedCard.title || "";
30022
- _cardLabels = (typedCard.labels || []).map((l) => l.name);
30023
- _cardDescription = typedCard.description || "";
30024
- _cardSubtasks = (typedCard.subtasks || []).map((s) => ({
30025
- title: s.title,
30026
- done: s.done
30027
- }));
30028
31169
  const projectId = typedCard.project_id;
30029
31170
  if (sessionStatus === "completed" && typedCard.labels?.length) {
30030
31171
  const agentLabel = typedCard.labels.find((l) => l.name.toLowerCase() === "agent");
@@ -30033,21 +31174,16 @@ async function handleToolCall(name, args, deps) {
30033
31174
  }
30034
31175
  }
30035
31176
  if (moveToColumn && projectId) {
30036
- const board = await client3.getBoard(projectId, {
30037
- summary: true
30038
- });
30039
- const columns = board.columns;
30040
- const col = columns.find((c) => c.name.toLowerCase().includes(moveToColumn.toLowerCase()));
30041
- if (col) {
30042
- await client3.moveCard(cardId, col.id);
30043
- movedTo = col.name;
30044
- }
31177
+ const col = await resolveColumnByName(client3, projectId, moveToColumn);
31178
+ await client3.moveCard(cardId, col.id);
31179
+ movedTo = col.name;
30045
31180
  }
30046
31181
  } catch {}
30047
31182
  const sessionObj = result.session;
30048
31183
  const pipelineResult = await runEndSessionPipeline(client3, deps, cardId, sessionStatus, endProgressPercent, sessionObj);
30049
31184
  return {
30050
31185
  success: true,
31186
+ ...sessionEndError && { sessionEndError },
30051
31187
  movedTo,
30052
31188
  learningsExtracted: pipelineResult.learningsExtracted,
30053
31189
  feedbackAdjusted: pipelineResult.feedbackAdjusted,
@@ -30789,38 +31925,27 @@ async function handleToolCall(name, args, deps) {
30789
31925
  const projectName = args.projectName || "My First Project";
30790
31926
  const template = args.template || "kanban";
30791
31927
  const keyName = args.keyName || "mcp-agent";
30792
- const apiUrl = deps.getApiUrl();
30793
- const signupResult = await signupUser(apiUrl, {
31928
+ const result = await onboardNewUser({
30794
31929
  email: email3,
30795
31930
  password,
30796
- full_name: fullName
30797
- });
30798
- const token = signupResult.session.access_token;
30799
- const workspaceResult = await requestWithBearer(apiUrl, token, "POST", "/workspaces", {
30800
- name: workspaceName
31931
+ fullName,
31932
+ workspaceName,
31933
+ projectName,
31934
+ template,
31935
+ keyName,
31936
+ apiUrl: deps.getApiUrl()
30801
31937
  });
30802
- const projectResult = await requestWithBearer(apiUrl, token, "POST", "/projects", {
30803
- workspaceId: workspaceResult.workspace.id,
30804
- name: projectName,
30805
- template
30806
- });
30807
- const keyResult = await requestWithBearer(apiUrl, token, "POST", "/api-keys", {
30808
- name: keyName
30809
- });
30810
- deps.saveConfig({ apiKey: keyResult.rawKey });
30811
- deps.setActiveWorkspace(workspaceResult.workspace.id);
30812
- deps.setActiveProject(projectResult.project.id);
31938
+ deps.saveConfig({ apiKey: result.apiKey.rawKey });
31939
+ deps.setActiveWorkspace(result.workspace.id);
31940
+ deps.setActiveProject(result.project.id);
30813
31941
  deps.resetClient();
30814
31942
  return {
30815
31943
  success: true,
30816
- user: signupResult.user,
30817
- workspace: workspaceResult.workspace,
30818
- project: projectResult.project,
30819
- columns: projectResult.columns,
30820
- apiKey: {
30821
- rawKey: keyResult.rawKey,
30822
- prefix: keyResult.apiKey.prefix
30823
- },
31944
+ user: result.user,
31945
+ workspace: result.workspace,
31946
+ project: result.project,
31947
+ columns: result.columns,
31948
+ apiKey: result.apiKey,
30824
31949
  message: `Onboarding complete! Account created for ${email3}. Workspace "${workspaceName}" and project "${projectName}" are ready. API key saved to config.`
30825
31950
  };
30826
31951
  }
@@ -30864,6 +31989,105 @@ async function handleToolCall(name, args, deps) {
30864
31989
  message: `Processed ${entitiesProcessed} entities, created ${relationsCreated} new relations.`
30865
31990
  };
30866
31991
  }
31992
+ case "harmony_audit_memories": {
31993
+ const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
31994
+ if (!workspaceId) {
31995
+ throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
31996
+ }
31997
+ const projectId = args.projectId || deps.getActiveProjectId() || undefined;
31998
+ const report = await runMemoryAudit(client3, workspaceId, projectId, {
31999
+ dryRun: args.dryRun,
32000
+ archiveBelow: args.archiveBelow,
32001
+ deleteBelow: args.deleteBelow,
32002
+ limit: args.limit,
32003
+ staleDraftAgeDays: args.staleDraftAgeDays
32004
+ });
32005
+ return {
32006
+ success: report.success,
32007
+ dryRun: report.dryRun,
32008
+ summary: report.summary,
32009
+ distribution: report.distribution,
32010
+ legacyBreakdown: report.legacyBreakdown,
32011
+ actionsTaken: report.actionsTaken,
32012
+ lowest: report.lowest,
32013
+ staleDrafts: report.staleDrafts,
32014
+ errors: report.errors,
32015
+ healthReport: report.healthReport
32016
+ };
32017
+ }
32018
+ case "harmony_cleanup_memories": {
32019
+ const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
32020
+ if (!workspaceId) {
32021
+ throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
32022
+ }
32023
+ const projectId = args.projectId || deps.getActiveProjectId() || undefined;
32024
+ const validSteps = [
32025
+ "prune",
32026
+ "consolidate",
32027
+ "orphans",
32028
+ "duplicates",
32029
+ "backfill",
32030
+ "audit"
32031
+ ];
32032
+ const rawSteps = args.steps;
32033
+ const steps = rawSteps?.filter((s) => validSteps.includes(s));
32034
+ if (rawSteps && steps && steps.length < rawSteps.length) {
32035
+ const invalid = rawSteps.filter((s) => !validSteps.includes(s));
32036
+ console.warn(`Unknown cleanup steps ignored: ${invalid.join(", ")}`);
32037
+ }
32038
+ const report = await runMemoryCleanup(client3, workspaceId, projectId, {
32039
+ dryRun: args.dryRun,
32040
+ steps,
32041
+ maxAgeDays: args.maxAgeDays,
32042
+ minClusterSize: args.minClusterSize,
32043
+ orphanAgeDays: args.orphanAgeDays,
32044
+ auditArchiveBelow: args.auditArchiveBelow,
32045
+ auditDeleteBelow: args.auditDeleteBelow
32046
+ });
32047
+ return {
32048
+ success: report.success,
32049
+ dryRun: report.dryRun,
32050
+ summary: report.summary,
32051
+ errors: report.errors,
32052
+ healthReport: report.healthReport
32053
+ };
32054
+ }
32055
+ case "harmony_purge_memories": {
32056
+ const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
32057
+ if (!workspaceId) {
32058
+ throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
32059
+ }
32060
+ const projectId = args.projectId || deps.getActiveProjectId();
32061
+ if (!projectId) {
32062
+ throw new Error("No project specified. Purge requires a project scope. Use harmony_set_project_context or provide projectId.");
32063
+ }
32064
+ const filters = {};
32065
+ if (args.tier)
32066
+ filters.tier = args.tier;
32067
+ if (args.scope)
32068
+ filters.scope = args.scope;
32069
+ if (args.type)
32070
+ filters.type = args.type;
32071
+ if (args.olderThanDays !== undefined)
32072
+ filters.olderThanDays = args.olderThanDays;
32073
+ if (args.maxConfidence !== undefined)
32074
+ filters.maxConfidence = args.maxConfidence;
32075
+ if (args.tags)
32076
+ filters.tags = args.tags;
32077
+ const report = await purgeMemories(client3, workspaceId, projectId, {
32078
+ dryRun: args.dryRun,
32079
+ filters
32080
+ });
32081
+ return {
32082
+ success: report.success,
32083
+ dryRun: report.dryRun,
32084
+ matched: report.matched,
32085
+ purged: report.purged,
32086
+ items: report.items,
32087
+ errors: report.errors,
32088
+ message: report.dryRun ? `Found ${report.matched} entities matching filters. Run with dryRun=false to delete.` : `Purged ${report.purged} of ${report.matched} matching entities.`
32089
+ };
32090
+ }
30867
32091
  default:
30868
32092
  throw new Error(`Unknown tool: ${name}`);
30869
32093
  }
@@ -30901,15 +32125,26 @@ class HarmonyMCPServer {
30901
32125
  const cv = this.server.getClientVersion();
30902
32126
  return cv ? { name: cv.name, version: cv.version } : null;
30903
32127
  });
32128
+ let exitCode = 0;
30904
32129
  const handleShutdown = async () => {
30905
32130
  try {
30906
32131
  await shutdownAllSessions();
30907
32132
  } catch {}
30908
32133
  destroyAutoSession();
30909
- process.exit(0);
32134
+ process.exit(exitCode);
30910
32135
  };
30911
32136
  process.on("SIGINT", handleShutdown);
30912
32137
  process.on("SIGTERM", handleShutdown);
32138
+ process.on("uncaughtException", (err) => {
32139
+ console.error("MCP server uncaught exception:", err);
32140
+ exitCode = 1;
32141
+ handleShutdown();
32142
+ });
32143
+ process.on("unhandledRejection", (reason) => {
32144
+ console.error("MCP server unhandled rejection:", reason);
32145
+ exitCode = 1;
32146
+ handleShutdown();
32147
+ });
30913
32148
  try {
30914
32149
  if (isConfigured()) {
30915
32150
  const workspaceId = getActiveWorkspaceId();
@@ -30931,17 +32166,37 @@ class HarmonyMCPServer {
30931
32166
 
30932
32167
  // src/remote.ts
30933
32168
  var HARMONY_API_URL = process.env.HARMONY_API_URL || "https://app.gethmy.com/api";
32169
+ var OAUTH_ISSUER = process.env.OAUTH_ISSUER || "https://app.gethmy.com";
32170
+ var PUBLIC_MCP_URL = process.env.PUBLIC_MCP_URL || "https://mcp.gethmy.com";
30934
32171
  var PORT = parseInt(process.env.PORT || "3002", 10);
30935
- async function validateApiKey(apiKey) {
32172
+ async function validateToken(token) {
32173
+ try {
32174
+ const response = await fetch(`${HARMONY_API_URL}/v1/auth/context`, {
32175
+ headers: { "X-API-Key": token }
32176
+ });
32177
+ if (!response.ok)
32178
+ return null;
32179
+ const data = await response.json();
32180
+ if (data.source === "jwt")
32181
+ return null;
32182
+ return {
32183
+ userId: data.userId,
32184
+ workspaceId: data.workspaceId,
32185
+ source: data.source
32186
+ };
32187
+ } catch {
32188
+ return null;
32189
+ }
32190
+ }
32191
+ async function resolveWorkspaceForLegacyKey(token) {
30936
32192
  try {
30937
32193
  const response = await fetch(`${HARMONY_API_URL}/v1/workspaces`, {
30938
- headers: { "X-API-Key": apiKey }
32194
+ headers: { "X-API-Key": token }
30939
32195
  });
30940
32196
  if (!response.ok)
30941
32197
  return null;
30942
32198
  const data = await response.json();
30943
- const firstWorkspace = data.workspaces?.[0];
30944
- return { workspaceId: firstWorkspace?.id ?? null };
32199
+ return data.workspaces?.[0]?.id ?? null;
30945
32200
  } catch {
30946
32201
  return null;
30947
32202
  }
@@ -31014,11 +32269,29 @@ app.get("/health", (c) => c.json({
31014
32269
  service: "harmony-mcp-remote",
31015
32270
  sessions: sessions.size
31016
32271
  }));
32272
+ app.get("/.well-known/oauth-protected-resource", (c) => c.json({
32273
+ resource: PUBLIC_MCP_URL,
32274
+ authorization_servers: [OAUTH_ISSUER],
32275
+ bearer_methods_supported: ["header"],
32276
+ resource_documentation: `${OAUTH_ISSUER}/docs/mcp`
32277
+ }));
32278
+ function unauthenticatedResponse() {
32279
+ return new Response(JSON.stringify({
32280
+ error: "unauthorized",
32281
+ error_description: "Missing or invalid access token"
32282
+ }), {
32283
+ status: 401,
32284
+ headers: {
32285
+ "Content-Type": "application/json",
32286
+ "WWW-Authenticate": `Bearer realm="mcp", resource_metadata="${PUBLIC_MCP_URL}/.well-known/oauth-protected-resource"`
32287
+ }
32288
+ });
32289
+ }
31017
32290
  app.all("/mcp", async (c) => {
31018
32291
  const method = c.req.method;
31019
32292
  const authHeader = c.req.header("Authorization");
31020
32293
  if (!authHeader?.startsWith("Bearer ")) {
31021
- return c.json({ error: "Missing Authorization: Bearer <api-key>" }, 401);
32294
+ return unauthenticatedResponse();
31022
32295
  }
31023
32296
  const apiKey = authHeader.slice(7);
31024
32297
  const sessionId = c.req.header("Mcp-Session-Id");
@@ -31027,9 +32300,12 @@ app.all("/mcp", async (c) => {
31027
32300
  return session.transport.handleRequest(c.req.raw);
31028
32301
  }
31029
32302
  if (method === "POST") {
31030
- const keyInfo = await validateApiKey(apiKey);
32303
+ const keyInfo = await validateToken(apiKey);
31031
32304
  if (!keyInfo) {
31032
- return c.json({ error: "Invalid API key" }, 401);
32305
+ return unauthenticatedResponse();
32306
+ }
32307
+ if (keyInfo.source === "api_key" && !keyInfo.workspaceId) {
32308
+ keyInfo.workspaceId = await resolveWorkspaceForLegacyKey(apiKey);
31033
32309
  }
31034
32310
  const session = createSession(apiKey, keyInfo);
31035
32311
  await session.server.connect(session.transport);