@gethmy/mcp 2.8.4 → 2.8.5
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 +430 -33
- package/dist/index.js +430 -33
- package/dist/lib/api-client.js +14 -4
- package/package.json +1 -1
- package/src/auto-session.ts +18 -9
- package/src/graph-expansion.ts +153 -0
- package/src/memory-park.ts +183 -4
- package/src/memory-provenance.ts +177 -0
- package/src/memory-tags.ts +88 -0
- package/src/server.ts +320 -43
package/dist/index.js
CHANGED
|
@@ -934,10 +934,14 @@ function getDisplayLinkType(linkType, direction) {
|
|
|
934
934
|
}
|
|
935
935
|
// ../harmony-shared/dist/commentSerializer.js
|
|
936
936
|
var CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
|
|
937
|
+
function sanitizeHeaderField(value) {
|
|
938
|
+
return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
|
|
939
|
+
}
|
|
937
940
|
function authorLabel(c) {
|
|
938
941
|
if (c.author_type === "agent")
|
|
939
942
|
return "AI agent";
|
|
940
|
-
|
|
943
|
+
const raw = c.author?.full_name || "teammate";
|
|
944
|
+
return sanitizeHeaderField(raw);
|
|
941
945
|
}
|
|
942
946
|
function criticalIds(comments) {
|
|
943
947
|
const keep = new Set;
|
|
@@ -994,9 +998,15 @@ function serializeCommentThread(comments, options = {}) {
|
|
|
994
998
|
if (c.resolved_at)
|
|
995
999
|
tags.push("resolved");
|
|
996
1000
|
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
997
|
-
const header = `[${ref(c.id)} | ${c.author_type} | ${authorLabel(c)} | ${c.comment_type} | ${c.created_at}${tagStr}]`;
|
|
998
|
-
|
|
999
|
-
|
|
1001
|
+
const header = `[${sanitizeHeaderField(ref(c.id))} | ${sanitizeHeaderField(c.author_type)} | ${authorLabel(c)} | ${sanitizeHeaderField(c.comment_type)} | ${sanitizeHeaderField(c.created_at)}${tagStr}]`;
|
|
1002
|
+
const fencedBody = c.body.trim().replaceAll("<", "<").replaceAll(">", ">");
|
|
1003
|
+
lines.push({
|
|
1004
|
+
at: c.created_at,
|
|
1005
|
+
text: `${header}
|
|
1006
|
+
<comment-body>
|
|
1007
|
+
${fencedBody}
|
|
1008
|
+
</comment-body>`
|
|
1009
|
+
});
|
|
1000
1010
|
}
|
|
1001
1011
|
for (const a of activity) {
|
|
1002
1012
|
const actor = a.actor ? `${a.actor} ` : "";
|
|
@@ -2023,7 +2033,6 @@ function resolveAgentIdentity(info) {
|
|
|
2023
2033
|
}
|
|
2024
2034
|
var AUTO_START_TRIGGERS = new Set([
|
|
2025
2035
|
"harmony_generate_prompt",
|
|
2026
|
-
"harmony_update_card",
|
|
2027
2036
|
"harmony_create_subtask",
|
|
2028
2037
|
"harmony_toggle_subtask",
|
|
2029
2038
|
"harmony_update_subtask"
|
|
@@ -2055,6 +2064,10 @@ async function trackActivity(cardId, options) {
|
|
|
2055
2064
|
const client3 = options?.client ?? clientGetter?.();
|
|
2056
2065
|
if (!client3)
|
|
2057
2066
|
return;
|
|
2067
|
+
const info = clientInfoGetter?.() ?? null;
|
|
2068
|
+
if (!info?.name)
|
|
2069
|
+
return;
|
|
2070
|
+
const { agentIdentifier, agentName } = resolveAgentIdentity(info);
|
|
2058
2071
|
const toEnd = [];
|
|
2059
2072
|
for (const [otherCardId, session] of activeSessions) {
|
|
2060
2073
|
if (otherCardId !== cardId && !session.isExplicit) {
|
|
@@ -2064,8 +2077,6 @@ async function trackActivity(cardId, options) {
|
|
|
2064
2077
|
for (const otherCardId of toEnd) {
|
|
2065
2078
|
await autoEndSession(client3, otherCardId, "completed");
|
|
2066
2079
|
}
|
|
2067
|
-
const info = clientInfoGetter?.() ?? null;
|
|
2068
|
-
const { agentIdentifier, agentName } = resolveAgentIdentity(info);
|
|
2069
2080
|
try {
|
|
2070
2081
|
await client3.startAgentSession(cardId, {
|
|
2071
2082
|
agentIdentifier,
|
|
@@ -2185,6 +2196,90 @@ async function autoExpandGraph(client3, entityId, title, content, _tags, workspa
|
|
|
2185
2196
|
return { relationsCreated: 0 };
|
|
2186
2197
|
}
|
|
2187
2198
|
}
|
|
2199
|
+
async function findSimilarEntities(client3, title, content, workspaceId, options) {
|
|
2200
|
+
const contentSnippet = content.slice(0, 200).trim();
|
|
2201
|
+
const query = [title, contentSnippet].filter(Boolean).join(" ");
|
|
2202
|
+
try {
|
|
2203
|
+
const { entities } = await client3.searchMemoryEntities(workspaceId, query, {
|
|
2204
|
+
project_id: options?.projectId,
|
|
2205
|
+
limit: options?.limit ?? 20,
|
|
2206
|
+
type: options?.type
|
|
2207
|
+
});
|
|
2208
|
+
const minScore = options?.minRrfScore ?? 0;
|
|
2209
|
+
const excludeSet = new Set(options?.excludeIds || []);
|
|
2210
|
+
return entities.filter((e) => {
|
|
2211
|
+
if (excludeSet.has(e.id))
|
|
2212
|
+
return false;
|
|
2213
|
+
if (minScore > 0 && (e.rrf_score ?? 0) < minScore)
|
|
2214
|
+
return false;
|
|
2215
|
+
return true;
|
|
2216
|
+
});
|
|
2217
|
+
} catch {
|
|
2218
|
+
return [];
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
var SUPERSEDE_RRF_THRESHOLD = 0.029;
|
|
2222
|
+
var SUPERSEDE_TITLE_OVERLAP = 0.5;
|
|
2223
|
+
var TITLE_STOPWORDS = new Set([
|
|
2224
|
+
"a",
|
|
2225
|
+
"an",
|
|
2226
|
+
"the",
|
|
2227
|
+
"and",
|
|
2228
|
+
"or",
|
|
2229
|
+
"of",
|
|
2230
|
+
"to",
|
|
2231
|
+
"in",
|
|
2232
|
+
"on",
|
|
2233
|
+
"for",
|
|
2234
|
+
"with",
|
|
2235
|
+
"is",
|
|
2236
|
+
"are",
|
|
2237
|
+
"be",
|
|
2238
|
+
"by",
|
|
2239
|
+
"at",
|
|
2240
|
+
"as"
|
|
2241
|
+
]);
|
|
2242
|
+
function significantTitleTokens(title) {
|
|
2243
|
+
return new Set(title.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2 && !TITLE_STOPWORDS.has(t)));
|
|
2244
|
+
}
|
|
2245
|
+
function jaccard(a, b) {
|
|
2246
|
+
if (a.size === 0 || b.size === 0)
|
|
2247
|
+
return 0;
|
|
2248
|
+
let intersection = 0;
|
|
2249
|
+
for (const t of a)
|
|
2250
|
+
if (b.has(t))
|
|
2251
|
+
intersection++;
|
|
2252
|
+
return intersection / (a.size + b.size - intersection);
|
|
2253
|
+
}
|
|
2254
|
+
async function findSupersedeCandidates(client3, title, content, type, workspaceId, options) {
|
|
2255
|
+
const rrfThreshold = options?.rrfThreshold ?? SUPERSEDE_RRF_THRESHOLD;
|
|
2256
|
+
const titleOverlap = options?.titleOverlap ?? SUPERSEDE_TITLE_OVERLAP;
|
|
2257
|
+
const candidateTokens = significantTitleTokens(title);
|
|
2258
|
+
try {
|
|
2259
|
+
const hits = await findSimilarEntities(client3, title, content, workspaceId, {
|
|
2260
|
+
projectId: options?.projectId,
|
|
2261
|
+
type,
|
|
2262
|
+
limit: options?.limit ?? 10,
|
|
2263
|
+
minRrfScore: rrfThreshold
|
|
2264
|
+
});
|
|
2265
|
+
return hits.filter((e) => {
|
|
2266
|
+
if (options?.scope && e.scope !== undefined) {
|
|
2267
|
+
if (e.scope !== options.scope)
|
|
2268
|
+
return false;
|
|
2269
|
+
}
|
|
2270
|
+
if (e.superseded_at) {
|
|
2271
|
+
return false;
|
|
2272
|
+
}
|
|
2273
|
+
return jaccard(candidateTokens, significantTitleTokens(e.title)) >= titleOverlap;
|
|
2274
|
+
}).map((e) => ({
|
|
2275
|
+
id: e.id,
|
|
2276
|
+
title: e.title,
|
|
2277
|
+
score: e.rrf_score ?? 0
|
|
2278
|
+
}));
|
|
2279
|
+
} catch {
|
|
2280
|
+
return [];
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2188
2283
|
|
|
2189
2284
|
// src/memory-floor.ts
|
|
2190
2285
|
var STOP_WORDS = new Set([
|
|
@@ -2323,6 +2418,10 @@ var DEFAULT_WEIGHTS = {
|
|
|
2323
2418
|
recency: 0.25,
|
|
2324
2419
|
importance: 0.2
|
|
2325
2420
|
};
|
|
2421
|
+
var USAGE_BUMP_SCALE = 0.6;
|
|
2422
|
+
var USAGE_BUMP_MAX = 2;
|
|
2423
|
+
var FEEDBACK_BUMP_SCALE = 0.8;
|
|
2424
|
+
var FEEDBACK_BUMP_MAX = 2;
|
|
2326
2425
|
var TYPE_TAU_SECONDS = {
|
|
2327
2426
|
preference: Number.POSITIVE_INFINITY,
|
|
2328
2427
|
pattern: 60 * 60 * 24 * 180,
|
|
@@ -2378,8 +2477,37 @@ function recencyDecay(lastAccessedAt, createdAt, type, now) {
|
|
|
2378
2477
|
const dtSec = Math.max(0, (now.getTime() - t) / 1000);
|
|
2379
2478
|
return clamp01(Math.exp(-dtSec / tau));
|
|
2380
2479
|
}
|
|
2381
|
-
function
|
|
2480
|
+
function baseImportance(raw, type) {
|
|
2382
2481
|
let v = typeof raw === "number" ? raw : TYPE_IMPORTANCE_DEFAULT[type] ?? 5;
|
|
2482
|
+
if (v < 1)
|
|
2483
|
+
v = 1;
|
|
2484
|
+
if (v > 10)
|
|
2485
|
+
v = 10;
|
|
2486
|
+
return v;
|
|
2487
|
+
}
|
|
2488
|
+
function usageBump(accessCount) {
|
|
2489
|
+
const n = typeof accessCount === "number" && accessCount > 0 ? accessCount : 0;
|
|
2490
|
+
if (n === 0)
|
|
2491
|
+
return 0;
|
|
2492
|
+
return Math.min(USAGE_BUMP_MAX, USAGE_BUMP_SCALE * Math.log(1 + n));
|
|
2493
|
+
}
|
|
2494
|
+
function feedbackBump(feedback) {
|
|
2495
|
+
const up = typeof feedback?.up === "number" ? feedback.up : 0;
|
|
2496
|
+
const down = typeof feedback?.down === "number" ? feedback.down : 0;
|
|
2497
|
+
const net = up - down;
|
|
2498
|
+
if (net === 0)
|
|
2499
|
+
return 0;
|
|
2500
|
+
const raw = FEEDBACK_BUMP_SCALE * Math.sign(net) * Math.log(1 + Math.abs(net));
|
|
2501
|
+
if (raw > FEEDBACK_BUMP_MAX)
|
|
2502
|
+
return FEEDBACK_BUMP_MAX;
|
|
2503
|
+
if (raw < -FEEDBACK_BUMP_MAX)
|
|
2504
|
+
return -FEEDBACK_BUMP_MAX;
|
|
2505
|
+
return raw;
|
|
2506
|
+
}
|
|
2507
|
+
function effectiveImportance(entity) {
|
|
2508
|
+
const base = baseImportance(entity.importance, entity.type);
|
|
2509
|
+
const bump = usageBump(entity.access_count) + feedbackBump(entity.metadata?.feedback);
|
|
2510
|
+
let v = base + bump;
|
|
2383
2511
|
if (v < 1)
|
|
2384
2512
|
v = 1;
|
|
2385
2513
|
if (v > 10)
|
|
@@ -2393,7 +2521,7 @@ function rescore(entities, options = {}) {
|
|
|
2393
2521
|
const scored = entities.map((entity) => {
|
|
2394
2522
|
const relevance = clamp01(relevanceMap.get(entity.id ?? "") ?? 0.5);
|
|
2395
2523
|
const recency = recencyDecay(entity.last_accessed_at, entity.created_at, entity.type, now);
|
|
2396
|
-
const importance =
|
|
2524
|
+
const importance = effectiveImportance(entity);
|
|
2397
2525
|
const score = w.relevance * relevance + w.recency * recency + w.importance * importance;
|
|
2398
2526
|
return { entity, relevance, recency, importance, score };
|
|
2399
2527
|
});
|
|
@@ -2406,6 +2534,11 @@ function rescore(entities, options = {}) {
|
|
|
2406
2534
|
});
|
|
2407
2535
|
return scored;
|
|
2408
2536
|
}
|
|
2537
|
+
function filterByMinConfidence(entities, minConfidence) {
|
|
2538
|
+
if (typeof minConfidence !== "number")
|
|
2539
|
+
return entities;
|
|
2540
|
+
return entities.filter((e) => typeof e.confidence === "number" && e.confidence >= minConfidence);
|
|
2541
|
+
}
|
|
2409
2542
|
function relevanceFromRank(ranked, decay = 10) {
|
|
2410
2543
|
const out = new Map;
|
|
2411
2544
|
ranked.forEach((entity, rank) => {
|
|
@@ -2438,6 +2571,82 @@ function fitToBudget(scored, budgetTokens) {
|
|
|
2438
2571
|
}
|
|
2439
2572
|
return out;
|
|
2440
2573
|
}
|
|
2574
|
+
function mergeFeedback(existing, vote) {
|
|
2575
|
+
const up = typeof existing?.up === "number" && existing.up > 0 ? existing.up : 0;
|
|
2576
|
+
const down = typeof existing?.down === "number" && existing.down > 0 ? existing.down : 0;
|
|
2577
|
+
return vote === "up" ? { up: up + 1, down } : { up, down: down + 1 };
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
// src/memory-provenance.ts
|
|
2581
|
+
function buildOrigin(input) {
|
|
2582
|
+
const origin = { source: input.source };
|
|
2583
|
+
if (input.source_card_id)
|
|
2584
|
+
origin.source_card_id = input.source_card_id;
|
|
2585
|
+
if (input.source_session_id)
|
|
2586
|
+
origin.source_session_id = input.source_session_id;
|
|
2587
|
+
if (input.author)
|
|
2588
|
+
origin.author = input.author;
|
|
2589
|
+
if (input.source_trust)
|
|
2590
|
+
origin.source_trust = input.source_trust;
|
|
2591
|
+
return origin;
|
|
2592
|
+
}
|
|
2593
|
+
function defaultConfidenceForSource(input) {
|
|
2594
|
+
if (input.source_trust === "document")
|
|
2595
|
+
return 0.9;
|
|
2596
|
+
if (input.source_trust === "manual")
|
|
2597
|
+
return 0.9;
|
|
2598
|
+
switch (input.source) {
|
|
2599
|
+
case "manual":
|
|
2600
|
+
return 0.9;
|
|
2601
|
+
case "assistant":
|
|
2602
|
+
return 0.8;
|
|
2603
|
+
case "agent-run":
|
|
2604
|
+
return 0.6;
|
|
2605
|
+
case "import":
|
|
2606
|
+
return 0.6;
|
|
2607
|
+
default:
|
|
2608
|
+
return 0.7;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
var TYPE_IMPORTANCE_DEFAULT2 = {
|
|
2612
|
+
preference: 9,
|
|
2613
|
+
lesson: 8,
|
|
2614
|
+
decision: 8,
|
|
2615
|
+
pattern: 7,
|
|
2616
|
+
solution: 7,
|
|
2617
|
+
procedure: 7,
|
|
2618
|
+
error: 5,
|
|
2619
|
+
context: 5,
|
|
2620
|
+
task: 5,
|
|
2621
|
+
agent: 5,
|
|
2622
|
+
relationship: 6,
|
|
2623
|
+
commitment: 7,
|
|
2624
|
+
project: 6,
|
|
2625
|
+
handoff: 6
|
|
2626
|
+
};
|
|
2627
|
+
var FILE_PATH = /(?:[\w.-]+\/){1,}[\w.-]+\.[a-z]{1,4}\b|(?:[\w-]+\/){2,}[\w-]+/i;
|
|
2628
|
+
var GUIDANCE_SECTION = /\b(why|how to apply|how to use|takeaway|root cause)\b/i;
|
|
2629
|
+
var PROPER_NOUN = /`[^`]+`|\b[A-Z][a-z0-9]+[A-Z][A-Za-z0-9]*\b|\b[a-z]+_[a-z][\w]*\b/;
|
|
2630
|
+
function signalDensity(content) {
|
|
2631
|
+
let n = 0;
|
|
2632
|
+
if (FILE_PATH.test(content))
|
|
2633
|
+
n += 1;
|
|
2634
|
+
if (GUIDANCE_SECTION.test(content))
|
|
2635
|
+
n += 1;
|
|
2636
|
+
if (PROPER_NOUN.test(content))
|
|
2637
|
+
n += 1;
|
|
2638
|
+
return n;
|
|
2639
|
+
}
|
|
2640
|
+
function importanceWithSignalBump(type, content) {
|
|
2641
|
+
const base = TYPE_IMPORTANCE_DEFAULT2[type] ?? 5;
|
|
2642
|
+
const bump = Math.min(2, signalDensity(content));
|
|
2643
|
+
const v = base + bump;
|
|
2644
|
+
if (v < 1)
|
|
2645
|
+
return 1;
|
|
2646
|
+
if (v > 10)
|
|
2647
|
+
return 10;
|
|
2648
|
+
return v;
|
|
2649
|
+
}
|
|
2441
2650
|
|
|
2442
2651
|
// src/memory-session.ts
|
|
2443
2652
|
var SESSION_SCOPE_PREFIX = "session:";
|
|
@@ -2458,6 +2667,49 @@ function sessionScopeFor(agentSessionId) {
|
|
|
2458
2667
|
return `${SESSION_SCOPE_PREFIX}${agentSessionId}`;
|
|
2459
2668
|
}
|
|
2460
2669
|
|
|
2670
|
+
// src/memory-tags.ts
|
|
2671
|
+
var CARD_REF = /^card[\s:#-]+(\d+)$/;
|
|
2672
|
+
function normalizeTag(tag) {
|
|
2673
|
+
const cleaned = tag.trim().toLowerCase().replace(/\s+/g, " ");
|
|
2674
|
+
const cardMatch = cleaned.match(CARD_REF);
|
|
2675
|
+
if (cardMatch) {
|
|
2676
|
+
return `card:${cardMatch[1]}`;
|
|
2677
|
+
}
|
|
2678
|
+
return cleaned;
|
|
2679
|
+
}
|
|
2680
|
+
function normalizeTags(tags) {
|
|
2681
|
+
const seen = new Set;
|
|
2682
|
+
const out = [];
|
|
2683
|
+
for (const raw of tags) {
|
|
2684
|
+
const norm = normalizeTag(raw);
|
|
2685
|
+
if (!norm || seen.has(norm))
|
|
2686
|
+
continue;
|
|
2687
|
+
seen.add(norm);
|
|
2688
|
+
out.push(norm);
|
|
2689
|
+
}
|
|
2690
|
+
return out;
|
|
2691
|
+
}
|
|
2692
|
+
function lintTags(tags) {
|
|
2693
|
+
const suggestions = [];
|
|
2694
|
+
for (const tag of tags) {
|
|
2695
|
+
const norm = normalizeTag(tag);
|
|
2696
|
+
if (norm === tag)
|
|
2697
|
+
continue;
|
|
2698
|
+
let reason;
|
|
2699
|
+
if (CARD_REF.test(tag.trim().toLowerCase()) && norm.startsWith("card:")) {
|
|
2700
|
+
reason = "non-canonical card ref; use 'card:<n>'";
|
|
2701
|
+
} else if (tag !== tag.trim() || /\s{2,}/.test(tag)) {
|
|
2702
|
+
reason = "whitespace; trim and collapse";
|
|
2703
|
+
} else if (tag !== tag.toLowerCase()) {
|
|
2704
|
+
reason = "case variant; use lowercase";
|
|
2705
|
+
} else {
|
|
2706
|
+
reason = "normalize to canonical form";
|
|
2707
|
+
}
|
|
2708
|
+
suggestions.push({ tag, suggestion: norm, reason });
|
|
2709
|
+
}
|
|
2710
|
+
return suggestions;
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2461
2713
|
// src/onboard.ts
|
|
2462
2714
|
async function onboardNewUser(params) {
|
|
2463
2715
|
const {
|
|
@@ -3675,6 +3927,10 @@ var TOOLS = {
|
|
|
3675
3927
|
projectId: {
|
|
3676
3928
|
type: "string",
|
|
3677
3929
|
description: "Project ID (optional, required if scope is 'project')"
|
|
3930
|
+
},
|
|
3931
|
+
supersedeId: {
|
|
3932
|
+
type: "string",
|
|
3933
|
+
description: "Optional. ID of an existing memory entity to soft-supersede with " + "this new one (sets its superseded_by/superseded_at). Use after a " + "previous harmony_remember returned a `similar` candidate you want " + "to replace. Reversible; never deletes or merges content. The new " + "entity is always created regardless."
|
|
3678
3934
|
}
|
|
3679
3935
|
},
|
|
3680
3936
|
required: ["title", "content"]
|
|
@@ -3848,6 +4104,49 @@ var TOOLS = {
|
|
|
3848
4104
|
required: ["sourceId", "targetId", "relationType"]
|
|
3849
4105
|
}
|
|
3850
4106
|
},
|
|
4107
|
+
harmony_suggest_relations: {
|
|
4108
|
+
description: "Suggest existing memory entities that look related to a given entity, using the same hybrid vector+FTS similarity as recall (card #281). Read-only: it NEVER creates relations — it only proposes candidates for a caller (human/agent) to relate explicitly via harmony_relate. Use to surface 'these look related — link?' candidates that would activate graph-walk retrieval.",
|
|
4109
|
+
inputSchema: {
|
|
4110
|
+
type: "object",
|
|
4111
|
+
properties: {
|
|
4112
|
+
entityId: {
|
|
4113
|
+
type: "string",
|
|
4114
|
+
description: "The entity UUID to find related candidates for"
|
|
4115
|
+
},
|
|
4116
|
+
limit: {
|
|
4117
|
+
type: "number",
|
|
4118
|
+
description: "Max suggestions to return (default: 5)"
|
|
4119
|
+
},
|
|
4120
|
+
workspaceId: {
|
|
4121
|
+
type: "string",
|
|
4122
|
+
description: "Workspace ID (optional if context set)"
|
|
4123
|
+
},
|
|
4124
|
+
projectId: {
|
|
4125
|
+
type: "string",
|
|
4126
|
+
description: "Project ID (optional)"
|
|
4127
|
+
}
|
|
4128
|
+
},
|
|
4129
|
+
required: ["entityId"]
|
|
4130
|
+
}
|
|
4131
|
+
},
|
|
4132
|
+
harmony_recall_feedback: {
|
|
4133
|
+
description: "Record a \uD83D\uDC4D/\uD83D\uDC4E on a recalled memory so future recalls can rank it better or worse. Non-destructive: stores a counter at metadata.feedback={up,down} and only affects ranking — it never deletes, hides, or supersedes the memory. Call after a recall when a memory proved useful (vote 'up') or misleading/irrelevant (vote 'down').",
|
|
4134
|
+
inputSchema: {
|
|
4135
|
+
type: "object",
|
|
4136
|
+
properties: {
|
|
4137
|
+
entityId: {
|
|
4138
|
+
type: "string",
|
|
4139
|
+
description: "Memory entity UUID that was recalled"
|
|
4140
|
+
},
|
|
4141
|
+
vote: {
|
|
4142
|
+
type: "string",
|
|
4143
|
+
enum: ["up", "down"],
|
|
4144
|
+
description: "'up' if the memory was useful, 'down' if it was misleading or irrelevant"
|
|
4145
|
+
}
|
|
4146
|
+
},
|
|
4147
|
+
required: ["entityId", "vote"]
|
|
4148
|
+
}
|
|
4149
|
+
},
|
|
3851
4150
|
harmony_memory_search: {
|
|
3852
4151
|
description: "Full-text and semantic search across the knowledge base. Uses hybrid vector+FTS search (when embeddings are available) for best results. Returns entities ranked by relevance.",
|
|
3853
4152
|
inputSchema: {
|
|
@@ -4925,7 +5224,9 @@ async function handleToolCall(name, args, deps) {
|
|
|
4925
5224
|
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
4926
5225
|
}
|
|
4927
5226
|
const entityType = args.type || "context";
|
|
4928
|
-
const
|
|
5227
|
+
const rawTags = args.tags || [];
|
|
5228
|
+
const entityTags = normalizeTags(rawTags);
|
|
5229
|
+
const tagSuggestions = lintTags(rawTags);
|
|
4929
5230
|
const activeMemSession = getActiveMemorySession();
|
|
4930
5231
|
const entityScope = resolveSessionScope(args.scope, activeMemSession?.agentSessionId) || "project";
|
|
4931
5232
|
const floorRejection = validateMemoryQuality({
|
|
@@ -4939,24 +5240,59 @@ async function handleToolCall(name, args, deps) {
|
|
|
4939
5240
|
if (floorRejection) {
|
|
4940
5241
|
throw new Error(`Memory rejected by Utility Floor [${floorRejection.rule}]: ${floorRejection.message}`);
|
|
4941
5242
|
}
|
|
4942
|
-
const
|
|
5243
|
+
const sourceTrust = args.source_trust;
|
|
5244
|
+
const source = activeMemSession ? activeMemSession.agentIdentifier === "assistant" ? "assistant" : "agent-run" : "manual";
|
|
5245
|
+
const origin = buildOrigin({
|
|
5246
|
+
source,
|
|
5247
|
+
source_card_id: activeMemSession?.cardId,
|
|
5248
|
+
source_session_id: activeMemSession?.agentSessionId,
|
|
5249
|
+
author: activeMemSession?.agentIdentifier ?? "user",
|
|
5250
|
+
source_trust: sourceTrust
|
|
5251
|
+
});
|
|
5252
|
+
const callerMetadata = args.metadata;
|
|
5253
|
+
const metadataWithOrigin = {
|
|
5254
|
+
...callerMetadata ?? {},
|
|
5255
|
+
origin
|
|
5256
|
+
};
|
|
5257
|
+
const confidence = args.confidence !== undefined ? z.number().min(0).max(1).parse(args.confidence) : defaultConfidenceForSource({ source, source_trust: sourceTrust });
|
|
5258
|
+
const importance = args.importance !== undefined ? z.number().int().min(1).max(10).parse(args.importance) : importanceWithSignalBump(entityType, content);
|
|
5259
|
+
const projectIdForWrite = args.projectId || deps.getActiveProjectId() || undefined;
|
|
5260
|
+
let similar = [];
|
|
5261
|
+
if (!isSessionScope(entityScope)) {
|
|
5262
|
+
similar = await findSupersedeCandidates(client3, title, content, entityType, workspaceId, { projectId: projectIdForWrite, scope: entityScope });
|
|
5263
|
+
}
|
|
4943
5264
|
const result = await client3.createMemoryEntity({
|
|
4944
5265
|
workspace_id: workspaceId,
|
|
4945
|
-
project_id:
|
|
5266
|
+
project_id: projectIdForWrite,
|
|
4946
5267
|
type: entityType,
|
|
4947
5268
|
scope: entityScope,
|
|
4948
5269
|
memory_tier: args.tier || undefined,
|
|
4949
5270
|
title,
|
|
4950
5271
|
content,
|
|
4951
|
-
metadata:
|
|
4952
|
-
confidence
|
|
4953
|
-
importance
|
|
5272
|
+
metadata: metadataWithOrigin,
|
|
5273
|
+
confidence,
|
|
5274
|
+
importance,
|
|
4954
5275
|
tags: entityTags.length > 0 ? entityTags : undefined,
|
|
4955
5276
|
agent_identifier: activeMemSession?.agentIdentifier || undefined
|
|
4956
5277
|
});
|
|
5278
|
+
const supersedeId = args.supersedeId;
|
|
5279
|
+
const newEntityId = result.entity?.id;
|
|
5280
|
+
let superseded;
|
|
5281
|
+
if (supersedeId && newEntityId) {
|
|
5282
|
+
try {
|
|
5283
|
+
await client3.updateMemoryEntity(supersedeId, {
|
|
5284
|
+
superseded_by: newEntityId,
|
|
5285
|
+
superseded_at: new Date().toISOString()
|
|
5286
|
+
});
|
|
5287
|
+
superseded = supersedeId;
|
|
5288
|
+
} catch (err) {
|
|
5289
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5290
|
+
console.debug(`[harmony_remember] supersede ${supersedeId} failed: ${msg}`);
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
4957
5293
|
const newEntityIdForGraph = result.entity?.id;
|
|
4958
5294
|
if (newEntityIdForGraph) {
|
|
4959
|
-
autoExpandGraph(client3, newEntityIdForGraph, title, content, entityTags, workspaceId,
|
|
5295
|
+
autoExpandGraph(client3, newEntityIdForGraph, title, content, entityTags, workspaceId, projectIdForWrite).catch(() => {});
|
|
4960
5296
|
}
|
|
4961
5297
|
if (activeMemSession) {
|
|
4962
5298
|
appendMemoryAction(activeMemSession.cardId, `Stored memory: ${title}`);
|
|
@@ -4964,7 +5300,10 @@ async function handleToolCall(name, args, deps) {
|
|
|
4964
5300
|
}
|
|
4965
5301
|
return {
|
|
4966
5302
|
success: true,
|
|
4967
|
-
...result
|
|
5303
|
+
...result,
|
|
5304
|
+
...similar.length > 0 ? { similar } : {},
|
|
5305
|
+
...superseded ? { superseded } : {},
|
|
5306
|
+
...tagSuggestions.length > 0 ? { tagSuggestions } : {}
|
|
4968
5307
|
};
|
|
4969
5308
|
}
|
|
4970
5309
|
case "harmony_recall": {
|
|
@@ -5003,9 +5342,7 @@ async function handleToolCall(name, args, deps) {
|
|
|
5003
5342
|
const wanted = new Set(requestedTags);
|
|
5004
5343
|
entities = entities.filter((e) => (e?.tags ?? []).some((t) => wanted.has(t)));
|
|
5005
5344
|
}
|
|
5006
|
-
|
|
5007
|
-
entities = entities.filter((e) => typeof e?.confidence === "number" && e.confidence >= minConfidence);
|
|
5008
|
-
}
|
|
5345
|
+
entities = filterByMinConfidence(entities, minConfidence);
|
|
5009
5346
|
if (!includeSuperseded) {
|
|
5010
5347
|
entities = entities.filter((e) => !e?.superseded_at);
|
|
5011
5348
|
}
|
|
@@ -5036,16 +5373,10 @@ async function handleToolCall(name, args, deps) {
|
|
|
5036
5373
|
trimmed = fitToBudget(trimmed, budgetTokens);
|
|
5037
5374
|
}
|
|
5038
5375
|
if (trimmed.length > 0) {
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
...entity.metadata || {},
|
|
5044
|
-
_last_recall: new Date().toISOString()
|
|
5045
|
-
}
|
|
5046
|
-
});
|
|
5047
|
-
} catch (_) {}
|
|
5048
|
-
})).catch(() => {});
|
|
5376
|
+
const touchIds = trimmed.map(({ entity }) => entity?.id).filter((id) => typeof id === "string");
|
|
5377
|
+
if (touchIds.length > 0) {
|
|
5378
|
+
client3.batchTouchMemoryEntities(touchIds).catch(() => {});
|
|
5379
|
+
}
|
|
5049
5380
|
}
|
|
5050
5381
|
let sessionEntities = [];
|
|
5051
5382
|
if (excludeSessionFromLongTerm && recallMemSession?.agentSessionId) {
|
|
@@ -5112,8 +5443,12 @@ async function handleToolCall(name, args, deps) {
|
|
|
5112
5443
|
updates.type = args.type;
|
|
5113
5444
|
if (args.scope !== undefined)
|
|
5114
5445
|
updates.scope = args.scope;
|
|
5115
|
-
|
|
5116
|
-
|
|
5446
|
+
let updateTagSuggestions = [];
|
|
5447
|
+
if (args.tags !== undefined) {
|
|
5448
|
+
const updateRawTags = args.tags;
|
|
5449
|
+
updates.tags = normalizeTags(updateRawTags);
|
|
5450
|
+
updateTagSuggestions = lintTags(updateRawTags);
|
|
5451
|
+
}
|
|
5117
5452
|
if (args.confidence !== undefined)
|
|
5118
5453
|
updates.confidence = z.number().min(0).max(1).parse(args.confidence);
|
|
5119
5454
|
if (args.metadata !== undefined)
|
|
@@ -5125,7 +5460,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
5125
5460
|
appendMemoryAction(updateMemSession.cardId, `Updated memory: ${updateTitle}`);
|
|
5126
5461
|
flushMemoryActions(client3, updateMemSession.cardId).catch(() => {});
|
|
5127
5462
|
}
|
|
5128
|
-
return {
|
|
5463
|
+
return {
|
|
5464
|
+
success: true,
|
|
5465
|
+
...result,
|
|
5466
|
+
...updateTagSuggestions.length > 0 ? { tagSuggestions: updateTagSuggestions } : {}
|
|
5467
|
+
};
|
|
5129
5468
|
}
|
|
5130
5469
|
case "harmony_forget": {
|
|
5131
5470
|
const entityId = z.string().uuid().parse(args.entityId);
|
|
@@ -5170,6 +5509,64 @@ async function handleToolCall(name, args, deps) {
|
|
|
5170
5509
|
}
|
|
5171
5510
|
return { success: true, ...result };
|
|
5172
5511
|
}
|
|
5512
|
+
case "harmony_suggest_relations": {
|
|
5513
|
+
const entityId = z.string().uuid().parse(args.entityId);
|
|
5514
|
+
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
|
5515
|
+
if (!workspaceId) {
|
|
5516
|
+
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
5517
|
+
}
|
|
5518
|
+
const projectId = args.projectId || deps.getActiveProjectId() || undefined;
|
|
5519
|
+
const limit = args.limit ?? 5;
|
|
5520
|
+
const { entity } = await client3.getMemoryEntity(entityId);
|
|
5521
|
+
const src = entity;
|
|
5522
|
+
if (!src) {
|
|
5523
|
+
throw new Error(`Entity not found: ${entityId}`);
|
|
5524
|
+
}
|
|
5525
|
+
const related = await client3.getRelatedEntities(entityId);
|
|
5526
|
+
const excludeIds = new Set([entityId]);
|
|
5527
|
+
for (const raw of [
|
|
5528
|
+
...related.outgoing || [],
|
|
5529
|
+
...related.incoming || []
|
|
5530
|
+
]) {
|
|
5531
|
+
const rel = raw;
|
|
5532
|
+
const target = rel.target;
|
|
5533
|
+
const source = rel.source;
|
|
5534
|
+
const otherId = target?.id ?? rel.target_id ?? source?.id ?? rel.source_id;
|
|
5535
|
+
if (otherId)
|
|
5536
|
+
excludeIds.add(otherId);
|
|
5537
|
+
}
|
|
5538
|
+
const suggestions = await findSimilarEntities(client3, src.title ?? "", src.content ?? "", workspaceId, {
|
|
5539
|
+
projectId,
|
|
5540
|
+
limit: limit + excludeIds.size,
|
|
5541
|
+
excludeIds: [...excludeIds]
|
|
5542
|
+
});
|
|
5543
|
+
return {
|
|
5544
|
+
success: true,
|
|
5545
|
+
suggestions: suggestions.slice(0, limit).map((s) => ({
|
|
5546
|
+
id: s.id,
|
|
5547
|
+
title: s.title,
|
|
5548
|
+
type: s.type,
|
|
5549
|
+
rrf_score: s.rrf_score
|
|
5550
|
+
}))
|
|
5551
|
+
};
|
|
5552
|
+
}
|
|
5553
|
+
case "harmony_recall_feedback": {
|
|
5554
|
+
const entityId = z.string().uuid().parse(args.entityId);
|
|
5555
|
+
const vote = z.enum(["up", "down"]).parse(args.vote);
|
|
5556
|
+
const { entity } = await client3.getMemoryEntity(entityId);
|
|
5557
|
+
const existingMeta = entity?.metadata ?? {};
|
|
5558
|
+
const existingFeedback = existingMeta.feedback;
|
|
5559
|
+
const feedback = mergeFeedback(existingFeedback, vote);
|
|
5560
|
+
const result = await client3.updateMemoryEntity(entityId, {
|
|
5561
|
+
metadata: { ...existingMeta, feedback }
|
|
5562
|
+
});
|
|
5563
|
+
const fbMemSession = getActiveMemorySession();
|
|
5564
|
+
if (fbMemSession) {
|
|
5565
|
+
appendMemoryAction(fbMemSession.cardId, `Memory feedback: ${vote === "up" ? "\uD83D\uDC4D" : "\uD83D\uDC4E"} ${entityId.slice(0, 8)}`);
|
|
5566
|
+
flushMemoryActions(client3, fbMemSession.cardId).catch(() => {});
|
|
5567
|
+
}
|
|
5568
|
+
return { success: true, feedback, ...result };
|
|
5569
|
+
}
|
|
5173
5570
|
case "harmony_memory_search": {
|
|
5174
5571
|
const query = z.string().min(1).max(500).parse(args.query);
|
|
5175
5572
|
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
package/dist/lib/api-client.js
CHANGED
|
@@ -726,10 +726,14 @@ function getDisplayLinkType(linkType, direction) {
|
|
|
726
726
|
}
|
|
727
727
|
// ../harmony-shared/dist/commentSerializer.js
|
|
728
728
|
var CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
|
|
729
|
+
function sanitizeHeaderField(value) {
|
|
730
|
+
return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
|
|
731
|
+
}
|
|
729
732
|
function authorLabel(c) {
|
|
730
733
|
if (c.author_type === "agent")
|
|
731
734
|
return "AI agent";
|
|
732
|
-
|
|
735
|
+
const raw = c.author?.full_name || "teammate";
|
|
736
|
+
return sanitizeHeaderField(raw);
|
|
733
737
|
}
|
|
734
738
|
function criticalIds(comments) {
|
|
735
739
|
const keep = new Set;
|
|
@@ -786,9 +790,15 @@ function serializeCommentThread(comments, options = {}) {
|
|
|
786
790
|
if (c.resolved_at)
|
|
787
791
|
tags.push("resolved");
|
|
788
792
|
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
789
|
-
const header = `[${ref(c.id)} | ${c.author_type} | ${authorLabel(c)} | ${c.comment_type} | ${c.created_at}${tagStr}]`;
|
|
790
|
-
|
|
791
|
-
|
|
793
|
+
const header = `[${sanitizeHeaderField(ref(c.id))} | ${sanitizeHeaderField(c.author_type)} | ${authorLabel(c)} | ${sanitizeHeaderField(c.comment_type)} | ${sanitizeHeaderField(c.created_at)}${tagStr}]`;
|
|
794
|
+
const fencedBody = c.body.trim().replaceAll("<", "<").replaceAll(">", ">");
|
|
795
|
+
lines.push({
|
|
796
|
+
at: c.created_at,
|
|
797
|
+
text: `${header}
|
|
798
|
+
<comment-body>
|
|
799
|
+
${fencedBody}
|
|
800
|
+
</comment-body>`
|
|
801
|
+
});
|
|
792
802
|
}
|
|
793
803
|
for (const a of activity) {
|
|
794
804
|
const actor = a.actor ? `${a.actor} ` : "";
|