@gethmy/mcp 2.4.3 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +4 -1
- package/dist/index.js +4 -1
- package/dist/remote.js +1739 -463
- package/package.json +1 -1
- package/src/__tests__/memory-audit.test.ts +60 -0
- package/src/api-client.ts +6 -0
- package/src/memory-audit.ts +10 -2
- package/src/remote.ts +87 -17
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
|
|
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
|
-
|
|
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
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
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
|
-
}
|
|
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
|
|
7214
|
-
|
|
7215
|
-
|
|
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
|
|
7228
|
-
|
|
7229
|
-
|
|
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
|
|
7241
|
-
|
|
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.
|
|
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:**
|
|
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
|
|
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
|
|
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(
|
|
27093
|
+
async function extractMidSessionLearnings(_client, ctx) {
|
|
26876
27094
|
const workspaceId = getActiveWorkspaceId();
|
|
26877
27095
|
if (!workspaceId)
|
|
26878
27096
|
return { count: 0, entityIds: [] };
|
|
26879
|
-
const
|
|
27097
|
+
const _projectId = getActiveProjectId() || undefined;
|
|
26880
27098
|
const now = Date.now();
|
|
26881
|
-
const
|
|
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:
|
|
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:
|
|
27143
|
+
lastExtractionAt: currentHistory?.lastExtractionAt ?? 0,
|
|
26985
27144
|
steps: currentHistory?.steps || []
|
|
26986
27145
|
});
|
|
26987
27146
|
}
|
|
26988
|
-
return { count:
|
|
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
|
-
|
|
27173
|
-
|
|
27174
|
-
|
|
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
|
-
|
|
27181
|
-
|
|
27182
|
-
|
|
27183
|
-
|
|
27184
|
-
|
|
27185
|
-
|
|
27186
|
-
|
|
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
|
-
|
|
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: `
|
|
27367
|
+
title: `Paused: ${session.cardTitle}`,
|
|
27197
27368
|
content: [
|
|
27198
|
-
`
|
|
27199
|
-
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.
|
|
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: "
|
|
27211
|
-
confidence: 0.
|
|
27380
|
+
tier: "draft",
|
|
27381
|
+
confidence: 0.6,
|
|
27212
27382
|
tags: [
|
|
27213
27383
|
"auto-extracted",
|
|
27214
|
-
"session-
|
|
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
|
|
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 ??
|
|
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,
|
|
27893
|
-
const suffix = topWords.length > 0 ? topWords.join("
|
|
27894
|
-
return
|
|
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/
|
|
27971
|
-
|
|
27972
|
-
|
|
27973
|
-
|
|
27974
|
-
|
|
27975
|
-
|
|
27976
|
-
|
|
27977
|
-
|
|
27978
|
-
|
|
27979
|
-
|
|
27980
|
-
|
|
27981
|
-
|
|
27982
|
-
|
|
27983
|
-
|
|
27984
|
-
|
|
27985
|
-
|
|
27986
|
-
|
|
27987
|
-
|
|
27988
|
-
|
|
27989
|
-
|
|
27990
|
-
|
|
27991
|
-
|
|
27992
|
-
|
|
27993
|
-
|
|
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
|
-
|
|
28003
|
-
const
|
|
28004
|
-
|
|
28005
|
-
|
|
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
|
-
|
|
28008
|
-
|
|
28009
|
-
|
|
28010
|
-
|
|
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"
|
|
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
|
-
|
|
29633
|
-
|
|
29634
|
-
|
|
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
|
|
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
|
-
|
|
30008
|
-
|
|
30009
|
-
|
|
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
|
|
30037
|
-
|
|
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
|
|
30793
|
-
const signupResult = await signupUser(apiUrl, {
|
|
31928
|
+
const result = await onboardNewUser({
|
|
30794
31929
|
email: email3,
|
|
30795
31930
|
password,
|
|
30796
|
-
|
|
30797
|
-
|
|
30798
|
-
|
|
30799
|
-
|
|
30800
|
-
|
|
31931
|
+
fullName,
|
|
31932
|
+
workspaceName,
|
|
31933
|
+
projectName,
|
|
31934
|
+
template,
|
|
31935
|
+
keyName,
|
|
31936
|
+
apiUrl: deps.getApiUrl()
|
|
30801
31937
|
});
|
|
30802
|
-
|
|
30803
|
-
|
|
30804
|
-
|
|
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:
|
|
30817
|
-
workspace:
|
|
30818
|
-
project:
|
|
30819
|
-
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(
|
|
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
|
|
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":
|
|
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
|
-
|
|
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
|
|
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
|
|
32303
|
+
const keyInfo = await validateToken(apiKey);
|
|
31031
32304
|
if (!keyInfo) {
|
|
31032
|
-
return
|
|
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);
|