@gethmy/mcp 2.4.7 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -1
- package/dist/cli.js +20826 -18366
- package/dist/index.js +20924 -18464
- package/dist/lib/api-client.js +122 -925
- package/package.json +2 -2
- package/src/__tests__/mcp-integration.test.ts +141 -0
- package/src/__tests__/memory-floor.test.ts +126 -0
- package/src/__tests__/memory-park.test.ts +213 -0
- package/src/__tests__/memory-session.test.ts +77 -0
- package/src/__tests__/prompt-builder.test.ts +234 -0
- package/src/__tests__/skills.test.ts +111 -0
- package/src/__tests__/tool-dispatch.test.ts +260 -0
- package/src/api-client.ts +129 -96
- package/src/memory-floor.ts +264 -0
- package/src/memory-park.ts +252 -0
- package/src/memory-session.ts +61 -0
- package/src/prompt-builder.ts +93 -0
- package/src/server.ts +351 -1467
- package/src/__tests__/active-learning.test.ts +0 -483
- package/src/__tests__/agent-performance-profiles.test.ts +0 -468
- package/src/__tests__/context-assembly.test.ts +0 -506
- package/src/__tests__/lifecycle-maintenance.test.ts +0 -238
- package/src/__tests__/memory-audit.test.ts +0 -528
- package/src/__tests__/pattern-detection.test.ts +0 -438
- package/src/active-learning.ts +0 -1165
- package/src/consolidation.ts +0 -383
- package/src/context-assembly.ts +0 -1175
- package/src/lifecycle-maintenance.ts +0 -120
- package/src/memory-audit.ts +0 -578
- package/src/memory-cleanup.ts +0 -902
package/src/server.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
discoverRelatedContext,
|
|
3
|
-
evaluateLifecycle,
|
|
4
|
-
syncFull,
|
|
5
|
-
syncPull,
|
|
6
|
-
syncPush,
|
|
7
|
-
} from "@harmony/memory";
|
|
1
|
+
import { syncFull, syncPull, syncPush } from "@harmony/memory";
|
|
8
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
4
|
import {
|
|
@@ -14,12 +8,6 @@ import {
|
|
|
14
8
|
ReadResourceRequestSchema,
|
|
15
9
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
16
10
|
import { z } from "zod";
|
|
17
|
-
import {
|
|
18
|
-
detectContradictions,
|
|
19
|
-
extractLearnings,
|
|
20
|
-
extractMidSessionLearnings,
|
|
21
|
-
type SessionContext,
|
|
22
|
-
} from "./active-learning.js";
|
|
23
11
|
import {
|
|
24
12
|
getClient,
|
|
25
13
|
type HarmonyApiClient,
|
|
@@ -46,26 +34,20 @@ import {
|
|
|
46
34
|
setActiveProject,
|
|
47
35
|
setActiveWorkspace,
|
|
48
36
|
} from "./config.js";
|
|
49
|
-
import { consolidateMemories } from "./consolidation.js";
|
|
50
|
-
import {
|
|
51
|
-
assembleContext,
|
|
52
|
-
cacheManifest,
|
|
53
|
-
computeRelevanceScore,
|
|
54
|
-
getCachedManifest,
|
|
55
|
-
mapToContextEntity,
|
|
56
|
-
recordContextFeedback,
|
|
57
|
-
trackSessionAssembly,
|
|
58
|
-
} from "./context-assembly.js";
|
|
59
37
|
import { autoExpandGraph } from "./graph-expansion.js";
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
38
|
+
import { validateMemoryQuality } from "./memory-floor.js";
|
|
39
|
+
import {
|
|
40
|
+
fitToBudget,
|
|
41
|
+
type ParkInput,
|
|
42
|
+
relevanceFromRank,
|
|
43
|
+
rescore,
|
|
44
|
+
} from "./memory-park.js";
|
|
62
45
|
import {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} from "./memory-
|
|
46
|
+
isSessionScope,
|
|
47
|
+
resolveSessionScope,
|
|
48
|
+
sessionScopeFor,
|
|
49
|
+
} from "./memory-session.js";
|
|
67
50
|
import { onboardNewUser } from "./onboard.js";
|
|
68
|
-
import type { PromptVariant } from "./prompt-builder.js";
|
|
69
51
|
|
|
70
52
|
/**
|
|
71
53
|
* Dependencies injected into tool handlers.
|
|
@@ -92,6 +74,11 @@ interface MemorySessionState {
|
|
|
92
74
|
cardId: string;
|
|
93
75
|
agentIdentifier: string;
|
|
94
76
|
agentName: string;
|
|
77
|
+
// Identifier of the live `card_agent_context` row for this card. Captured on
|
|
78
|
+
// session start so memory tools can scope working-memory writes to it
|
|
79
|
+
// without a round-trip. May be undefined if the start endpoint did not
|
|
80
|
+
// return an id (older clients) — `scope: 'session'` will then refuse.
|
|
81
|
+
agentSessionId?: string;
|
|
95
82
|
memoryReadCount: number;
|
|
96
83
|
pendingActions: { action: string; ts: string }[];
|
|
97
84
|
allActions: { action: string; ts: string }[];
|
|
@@ -143,11 +130,13 @@ function initMemorySession(
|
|
|
143
130
|
cardId: string,
|
|
144
131
|
agentIdentifier: string,
|
|
145
132
|
agentName: string,
|
|
133
|
+
agentSessionId?: string,
|
|
146
134
|
): void {
|
|
147
135
|
memorySessions.set(cardId, {
|
|
148
136
|
cardId,
|
|
149
137
|
agentIdentifier,
|
|
150
138
|
agentName,
|
|
139
|
+
agentSessionId,
|
|
151
140
|
memoryReadCount: 0,
|
|
152
141
|
pendingActions: [],
|
|
153
142
|
allActions: [],
|
|
@@ -274,7 +263,7 @@ function cleanupMemorySession(cardId: string): void {
|
|
|
274
263
|
}
|
|
275
264
|
|
|
276
265
|
// Tool definitions
|
|
277
|
-
const TOOLS = {
|
|
266
|
+
export const TOOLS = {
|
|
278
267
|
// Card operations
|
|
279
268
|
harmony_create_card: {
|
|
280
269
|
description: "Create a new card in a Kanban column",
|
|
@@ -837,25 +826,6 @@ const TOOLS = {
|
|
|
837
826
|
},
|
|
838
827
|
},
|
|
839
828
|
|
|
840
|
-
harmony_get_agent_profile: {
|
|
841
|
-
description:
|
|
842
|
-
"Get aggregate performance profile for an agent. Shows total sessions, completion rate, average duration, and more. Defaults to the current agent if no identifier provided.",
|
|
843
|
-
inputSchema: {
|
|
844
|
-
type: "object",
|
|
845
|
-
properties: {
|
|
846
|
-
agentIdentifier: {
|
|
847
|
-
type: "string",
|
|
848
|
-
description:
|
|
849
|
-
"Agent identifier (e.g., 'claude-code'). Defaults to current agent.",
|
|
850
|
-
},
|
|
851
|
-
workspaceId: {
|
|
852
|
-
type: "string",
|
|
853
|
-
description: "Workspace ID (optional if context set)",
|
|
854
|
-
},
|
|
855
|
-
},
|
|
856
|
-
},
|
|
857
|
-
},
|
|
858
|
-
|
|
859
829
|
// Prompt generation
|
|
860
830
|
harmony_generate_prompt: {
|
|
861
831
|
description:
|
|
@@ -939,9 +909,12 @@ const TOOLS = {
|
|
|
939
909
|
},
|
|
940
910
|
scope: {
|
|
941
911
|
type: "string",
|
|
942
|
-
enum: ["private", "project", "workspace", "global"],
|
|
912
|
+
enum: ["private", "project", "workspace", "global", "session"],
|
|
943
913
|
description:
|
|
944
|
-
"Visibility scope (default: project). Private = only creator can see."
|
|
914
|
+
"Visibility scope (default: project). Private = only creator can see. " +
|
|
915
|
+
"Use 'session' for ephemeral working memory bound to the active agent " +
|
|
916
|
+
"session — auto-expires ~24h after the session ends and is excluded from " +
|
|
917
|
+
"the long-term vault.",
|
|
945
918
|
},
|
|
946
919
|
tier: {
|
|
947
920
|
type: "string",
|
|
@@ -958,6 +931,16 @@ const TOOLS = {
|
|
|
958
931
|
type: "number",
|
|
959
932
|
description: "Confidence score 0-1 (default: 1.0)",
|
|
960
933
|
},
|
|
934
|
+
importance: {
|
|
935
|
+
type: "number",
|
|
936
|
+
description:
|
|
937
|
+
"Importance score 1-10 for Park-style retrieval ranking (plan §6.2). If omitted, falls back to per-type default (preference=9, lesson/decision=8, pattern/solution/procedure=7, error/context=5).",
|
|
938
|
+
},
|
|
939
|
+
auto_score_importance: {
|
|
940
|
+
type: "boolean",
|
|
941
|
+
description:
|
|
942
|
+
"If true and `importance` is not provided, run an LLM-based rating instead of the per-type default. Costs ~500ms-2s + tokens. Default false (plan §7.1).",
|
|
943
|
+
},
|
|
961
944
|
metadata: {
|
|
962
945
|
type: "object",
|
|
963
946
|
description: "Additional structured metadata",
|
|
@@ -1007,8 +990,11 @@ const TOOLS = {
|
|
|
1007
990
|
},
|
|
1008
991
|
scope: {
|
|
1009
992
|
type: "string",
|
|
1010
|
-
enum: ["private", "project", "workspace", "global"],
|
|
1011
|
-
description:
|
|
993
|
+
enum: ["private", "project", "workspace", "global", "session"],
|
|
994
|
+
description:
|
|
995
|
+
"Filter by scope. 'session' returns only working memory for the " +
|
|
996
|
+
"active agent session. When omitted and a session is active, " +
|
|
997
|
+
"session memories are prepended to the long-term result.",
|
|
1012
998
|
},
|
|
1013
999
|
query: {
|
|
1014
1000
|
type: "string",
|
|
@@ -1027,6 +1013,21 @@ const TOOLS = {
|
|
|
1027
1013
|
description: "Project ID (optional)",
|
|
1028
1014
|
},
|
|
1029
1015
|
limit: { type: "number", description: "Max results (default: 20)" },
|
|
1016
|
+
top_k: {
|
|
1017
|
+
type: "number",
|
|
1018
|
+
description:
|
|
1019
|
+
"After Park rescoring (relevance × recency × importance per plan §6), keep this many top entries. Defaults to `limit`. Useful when you want to over-fetch (large `limit`) so the rescorer has more candidates and then trim to a tight top-k.",
|
|
1020
|
+
},
|
|
1021
|
+
budget_tokens: {
|
|
1022
|
+
type: "number",
|
|
1023
|
+
description:
|
|
1024
|
+
"If set, after rescoring + top-k, greedy-fill into a token budget (each entity = title + first 200 chars of content). Returns the prefix that fits.",
|
|
1025
|
+
},
|
|
1026
|
+
include_superseded: {
|
|
1027
|
+
type: "boolean",
|
|
1028
|
+
description:
|
|
1029
|
+
"Include rows whose `superseded_at` is set (default false). Use when investigating supersede chains.",
|
|
1030
|
+
},
|
|
1030
1031
|
},
|
|
1031
1032
|
},
|
|
1032
1033
|
},
|
|
@@ -1397,130 +1398,6 @@ const TOOLS = {
|
|
|
1397
1398
|
},
|
|
1398
1399
|
},
|
|
1399
1400
|
|
|
1400
|
-
// Memory lifecycle
|
|
1401
|
-
harmony_prune_draft: {
|
|
1402
|
-
description:
|
|
1403
|
-
"Remove stale draft memories that haven't been accessed recently. Runs in dry-run mode by default to preview what would be pruned.",
|
|
1404
|
-
inputSchema: {
|
|
1405
|
-
type: "object",
|
|
1406
|
-
properties: {
|
|
1407
|
-
workspaceId: {
|
|
1408
|
-
type: "string",
|
|
1409
|
-
description: "Workspace ID (optional if context set)",
|
|
1410
|
-
},
|
|
1411
|
-
projectId: {
|
|
1412
|
-
type: "string",
|
|
1413
|
-
description: "Project ID (optional)",
|
|
1414
|
-
},
|
|
1415
|
-
dryRun: {
|
|
1416
|
-
type: "boolean",
|
|
1417
|
-
description:
|
|
1418
|
-
"Preview what would be pruned without deleting (default: true)",
|
|
1419
|
-
},
|
|
1420
|
-
maxAgeDays: {
|
|
1421
|
-
type: "number",
|
|
1422
|
-
description:
|
|
1423
|
-
"Maximum age in days for draft memories to keep (default: 30)",
|
|
1424
|
-
},
|
|
1425
|
-
},
|
|
1426
|
-
required: [],
|
|
1427
|
-
},
|
|
1428
|
-
},
|
|
1429
|
-
|
|
1430
|
-
harmony_consolidate_memories: {
|
|
1431
|
-
description:
|
|
1432
|
-
"Consolidate similar draft/episode memories into reference entities. Groups similar memories by embedding similarity and merges clusters. Use dryRun to preview before executing.",
|
|
1433
|
-
inputSchema: {
|
|
1434
|
-
type: "object",
|
|
1435
|
-
properties: {
|
|
1436
|
-
workspaceId: {
|
|
1437
|
-
type: "string",
|
|
1438
|
-
description: "Workspace ID (optional if context set)",
|
|
1439
|
-
},
|
|
1440
|
-
projectId: {
|
|
1441
|
-
type: "string",
|
|
1442
|
-
description: "Project ID (optional)",
|
|
1443
|
-
},
|
|
1444
|
-
dryRun: {
|
|
1445
|
-
type: "boolean",
|
|
1446
|
-
description:
|
|
1447
|
-
"Preview what would be consolidated without creating entities (default: true)",
|
|
1448
|
-
},
|
|
1449
|
-
minClusterSize: {
|
|
1450
|
-
type: "number",
|
|
1451
|
-
description:
|
|
1452
|
-
"Minimum number of similar entities to form a cluster (default: 2)",
|
|
1453
|
-
},
|
|
1454
|
-
},
|
|
1455
|
-
required: [],
|
|
1456
|
-
},
|
|
1457
|
-
},
|
|
1458
|
-
|
|
1459
|
-
// Context assembly
|
|
1460
|
-
harmony_get_context_manifest: {
|
|
1461
|
-
description:
|
|
1462
|
-
"Retrieve the context assembly manifest for a given assembly ID to debug what memories were loaded or excluded and why.",
|
|
1463
|
-
inputSchema: {
|
|
1464
|
-
type: "object",
|
|
1465
|
-
properties: {
|
|
1466
|
-
assemblyId: {
|
|
1467
|
-
type: "string",
|
|
1468
|
-
description:
|
|
1469
|
-
"Assembly ID from a previous harmony_generate_prompt call",
|
|
1470
|
-
},
|
|
1471
|
-
},
|
|
1472
|
-
required: ["assemblyId"],
|
|
1473
|
-
},
|
|
1474
|
-
},
|
|
1475
|
-
|
|
1476
|
-
// Debug & visualization
|
|
1477
|
-
harmony_debug_context: {
|
|
1478
|
-
description:
|
|
1479
|
-
"Explain why a specific memory was or wasn't included in context assembly for a given task. Returns relevance score breakdown.",
|
|
1480
|
-
inputSchema: {
|
|
1481
|
-
type: "object",
|
|
1482
|
-
properties: {
|
|
1483
|
-
entityId: {
|
|
1484
|
-
type: "string",
|
|
1485
|
-
description: "Memory entity ID to analyze",
|
|
1486
|
-
},
|
|
1487
|
-
taskContext: {
|
|
1488
|
-
type: "string",
|
|
1489
|
-
description:
|
|
1490
|
-
"Task context (card title + description) to score against",
|
|
1491
|
-
},
|
|
1492
|
-
cardLabels: {
|
|
1493
|
-
type: "array",
|
|
1494
|
-
items: { type: "string" },
|
|
1495
|
-
description: "Card labels for tag matching",
|
|
1496
|
-
},
|
|
1497
|
-
},
|
|
1498
|
-
required: ["entityId", "taskContext"],
|
|
1499
|
-
},
|
|
1500
|
-
},
|
|
1501
|
-
harmony_export_memory_graph: {
|
|
1502
|
-
description:
|
|
1503
|
-
"Export the knowledge graph as DOT format for Graphviz visualization. Nodes are colored by tier (draft=yellow, episode=blue, reference=green).",
|
|
1504
|
-
inputSchema: {
|
|
1505
|
-
type: "object",
|
|
1506
|
-
properties: {
|
|
1507
|
-
workspaceId: {
|
|
1508
|
-
type: "string",
|
|
1509
|
-
description: "Workspace ID (optional if context set)",
|
|
1510
|
-
},
|
|
1511
|
-
projectId: {
|
|
1512
|
-
type: "string",
|
|
1513
|
-
description: "Project ID (optional)",
|
|
1514
|
-
},
|
|
1515
|
-
limit: {
|
|
1516
|
-
type: "number",
|
|
1517
|
-
description: "Max entities to include (default: 50)",
|
|
1518
|
-
},
|
|
1519
|
-
},
|
|
1520
|
-
required: [],
|
|
1521
|
-
},
|
|
1522
|
-
},
|
|
1523
|
-
|
|
1524
1401
|
// ============ ONBOARDING TOOLS ============
|
|
1525
1402
|
harmony_signup: {
|
|
1526
1403
|
description:
|
|
@@ -1641,299 +1518,47 @@ const TOOLS = {
|
|
|
1641
1518
|
required: ["email", "password", "fullName"],
|
|
1642
1519
|
},
|
|
1643
1520
|
},
|
|
1521
|
+
};
|
|
1644
1522
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
entityId: {
|
|
1653
|
-
type: "string",
|
|
1654
|
-
description: "Memory entity ID to promote",
|
|
1655
|
-
},
|
|
1656
|
-
targetTier: {
|
|
1657
|
-
type: "string",
|
|
1658
|
-
enum: ["episode", "reference"],
|
|
1659
|
-
description: "Target tier to promote to",
|
|
1660
|
-
},
|
|
1661
|
-
reason: {
|
|
1662
|
-
type: "string",
|
|
1663
|
-
description:
|
|
1664
|
-
"Reason for promotion (e.g., 'proven useful across 5+ sessions')",
|
|
1665
|
-
},
|
|
1666
|
-
},
|
|
1667
|
-
required: ["entityId", "targetTier", "reason"],
|
|
1668
|
-
},
|
|
1523
|
+
// Resource URIs
|
|
1524
|
+
export const RESOURCES = [
|
|
1525
|
+
{
|
|
1526
|
+
uri: "harmony://context",
|
|
1527
|
+
name: "Current Context",
|
|
1528
|
+
description: "Current active workspace and project",
|
|
1529
|
+
mimeType: "application/json",
|
|
1669
1530
|
},
|
|
1531
|
+
];
|
|
1670
1532
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
projectId: {
|
|
1701
|
-
type: "string",
|
|
1702
|
-
description: "Project ID (optional, limits to project)",
|
|
1703
|
-
},
|
|
1704
|
-
maxRelationsPerEntity: {
|
|
1705
|
-
type: "number",
|
|
1706
|
-
description: "Maximum relations to create per entity (default: 3)",
|
|
1707
|
-
},
|
|
1708
|
-
},
|
|
1709
|
-
required: [],
|
|
1710
|
-
},
|
|
1711
|
-
},
|
|
1712
|
-
|
|
1713
|
-
harmony_cleanup_memories: {
|
|
1714
|
-
description:
|
|
1715
|
-
"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 — run with dryRun=false to execute.",
|
|
1716
|
-
inputSchema: {
|
|
1717
|
-
type: "object",
|
|
1718
|
-
properties: {
|
|
1719
|
-
workspaceId: {
|
|
1720
|
-
type: "string",
|
|
1721
|
-
description: "Workspace ID (optional if context set)",
|
|
1722
|
-
},
|
|
1723
|
-
projectId: {
|
|
1724
|
-
type: "string",
|
|
1725
|
-
description: "Project ID (optional)",
|
|
1726
|
-
},
|
|
1727
|
-
dryRun: {
|
|
1728
|
-
type: "boolean",
|
|
1729
|
-
description:
|
|
1730
|
-
"Preview cleanup without executing changes (default: true)",
|
|
1731
|
-
},
|
|
1732
|
-
steps: {
|
|
1733
|
-
type: "array",
|
|
1734
|
-
items: {
|
|
1735
|
-
type: "string",
|
|
1736
|
-
enum: [
|
|
1737
|
-
"prune",
|
|
1738
|
-
"consolidate",
|
|
1739
|
-
"orphans",
|
|
1740
|
-
"duplicates",
|
|
1741
|
-
"backfill",
|
|
1742
|
-
"audit",
|
|
1743
|
-
],
|
|
1744
|
-
},
|
|
1745
|
-
description:
|
|
1746
|
-
"Which cleanup steps to run (default: all). Options: prune, consolidate, orphans, duplicates, backfill, audit.",
|
|
1747
|
-
},
|
|
1748
|
-
maxAgeDays: {
|
|
1749
|
-
type: "number",
|
|
1750
|
-
description: "Max age in days for stale draft pruning (default: 30)",
|
|
1751
|
-
},
|
|
1752
|
-
minClusterSize: {
|
|
1753
|
-
type: "number",
|
|
1754
|
-
description: "Min cluster size for consolidation (default: 3)",
|
|
1755
|
-
},
|
|
1756
|
-
orphanAgeDays: {
|
|
1757
|
-
type: "number",
|
|
1758
|
-
description: "Min age in days for orphan detection (default: 14)",
|
|
1759
|
-
},
|
|
1760
|
-
auditArchiveBelow: {
|
|
1761
|
-
type: "number",
|
|
1762
|
-
description:
|
|
1763
|
-
"Audit: archive entities scoring below this (default: 40)",
|
|
1764
|
-
},
|
|
1765
|
-
auditDeleteBelow: {
|
|
1766
|
-
type: "number",
|
|
1767
|
-
description:
|
|
1768
|
-
"Audit: delete entities scoring below this (default: 20)",
|
|
1769
|
-
},
|
|
1770
|
-
},
|
|
1771
|
-
required: [],
|
|
1772
|
-
},
|
|
1773
|
-
},
|
|
1774
|
-
harmony_audit_memories: {
|
|
1775
|
-
description:
|
|
1776
|
-
"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 (≥70), review (40-69), archive (20-39), delete (<20). Dry-run by default.",
|
|
1777
|
-
inputSchema: {
|
|
1778
|
-
type: "object",
|
|
1779
|
-
properties: {
|
|
1780
|
-
workspaceId: {
|
|
1781
|
-
type: "string",
|
|
1782
|
-
description: "Workspace ID (optional if context set)",
|
|
1783
|
-
},
|
|
1784
|
-
projectId: {
|
|
1785
|
-
type: "string",
|
|
1786
|
-
description: "Project ID (optional)",
|
|
1787
|
-
},
|
|
1788
|
-
dryRun: {
|
|
1789
|
-
type: "boolean",
|
|
1790
|
-
description:
|
|
1791
|
-
"Preview audit without flagging/archiving/deleting (default: true)",
|
|
1792
|
-
},
|
|
1793
|
-
archiveBelow: {
|
|
1794
|
-
type: "number",
|
|
1795
|
-
description:
|
|
1796
|
-
"Score threshold below which entities are archived (confidence set to 0.25). Default: 40",
|
|
1797
|
-
},
|
|
1798
|
-
deleteBelow: {
|
|
1799
|
-
type: "number",
|
|
1800
|
-
description:
|
|
1801
|
-
"Score threshold below which entities are hard-deleted. Default: 20. Set to 0 to never delete.",
|
|
1802
|
-
},
|
|
1803
|
-
limit: {
|
|
1804
|
-
type: "number",
|
|
1805
|
-
description:
|
|
1806
|
-
"Max number of entities to audit (default: 500). Paginated fetch.",
|
|
1807
|
-
},
|
|
1808
|
-
staleDraftAgeDays: {
|
|
1809
|
-
type: "number",
|
|
1810
|
-
description:
|
|
1811
|
-
"Age threshold (days) for the stale-draft filter: flags drafts with 0 accesses older than this. Reported separately from bucket scoring — surfaces promote-or-drop candidates the thresholds miss. Default: 7.",
|
|
1812
|
-
},
|
|
1813
|
-
},
|
|
1814
|
-
required: [],
|
|
1815
|
-
},
|
|
1816
|
-
},
|
|
1817
|
-
harmony_purge_memories: {
|
|
1818
|
-
description:
|
|
1819
|
-
"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 — preview what would be deleted before executing.",
|
|
1820
|
-
inputSchema: {
|
|
1821
|
-
type: "object",
|
|
1822
|
-
properties: {
|
|
1823
|
-
workspaceId: {
|
|
1824
|
-
type: "string",
|
|
1825
|
-
description: "Workspace ID (optional if context set)",
|
|
1826
|
-
},
|
|
1827
|
-
projectId: {
|
|
1828
|
-
type: "string",
|
|
1829
|
-
description:
|
|
1830
|
-
"Project ID (required — purge is project-scoped). Falls back to active project context.",
|
|
1831
|
-
},
|
|
1832
|
-
dryRun: {
|
|
1833
|
-
type: "boolean",
|
|
1834
|
-
description:
|
|
1835
|
-
"Preview what would be deleted without executing (default: true)",
|
|
1836
|
-
},
|
|
1837
|
-
tier: {
|
|
1838
|
-
type: "string",
|
|
1839
|
-
enum: ["draft", "episode", "reference"],
|
|
1840
|
-
description: 'Filter by memory tier (e.g. "draft")',
|
|
1841
|
-
},
|
|
1842
|
-
scope: {
|
|
1843
|
-
type: "string",
|
|
1844
|
-
description:
|
|
1845
|
-
'Filter by scope (e.g. "private", "project", "workspace")',
|
|
1846
|
-
},
|
|
1847
|
-
type: {
|
|
1848
|
-
type: "string",
|
|
1849
|
-
description:
|
|
1850
|
-
'Filter by entity type (e.g. "error", "pattern", "lesson", "decision")',
|
|
1851
|
-
},
|
|
1852
|
-
olderThanDays: {
|
|
1853
|
-
type: "number",
|
|
1854
|
-
description:
|
|
1855
|
-
"Only include entities not accessed in at least this many days",
|
|
1856
|
-
},
|
|
1857
|
-
maxConfidence: {
|
|
1858
|
-
type: "number",
|
|
1859
|
-
description:
|
|
1860
|
-
"Only include entities with confidence at or below this value (e.g. 0.3 for low-confidence junk)",
|
|
1861
|
-
},
|
|
1862
|
-
tags: {
|
|
1863
|
-
type: "array",
|
|
1864
|
-
items: { type: "string" },
|
|
1865
|
-
description: "Only include entities matching these tags",
|
|
1866
|
-
},
|
|
1867
|
-
},
|
|
1868
|
-
required: [],
|
|
1869
|
-
},
|
|
1870
|
-
},
|
|
1871
|
-
};
|
|
1872
|
-
|
|
1873
|
-
// Resource URIs
|
|
1874
|
-
const RESOURCES = [
|
|
1875
|
-
{
|
|
1876
|
-
uri: "harmony://context",
|
|
1877
|
-
name: "Current Context",
|
|
1878
|
-
description: "Current active workspace and project",
|
|
1879
|
-
mimeType: "application/json",
|
|
1880
|
-
},
|
|
1881
|
-
];
|
|
1882
|
-
|
|
1883
|
-
/**
|
|
1884
|
-
* Reusable end-session pipeline: learning extraction, feedback scoring, lifecycle maintenance.
|
|
1885
|
-
* Called by both explicit harmony_end_agent_session and auto-session timeout/card-switch.
|
|
1886
|
-
*/
|
|
1887
|
-
export async function runEndSessionPipeline(
|
|
1888
|
-
client: HarmonyApiClient,
|
|
1889
|
-
deps: ToolDeps,
|
|
1890
|
-
cardId: string,
|
|
1891
|
-
sessionStatus: "completed" | "paused",
|
|
1892
|
-
endProgressPercent?: number,
|
|
1893
|
-
sessionData?: {
|
|
1894
|
-
agent_identifier?: string;
|
|
1895
|
-
agent_name?: string;
|
|
1896
|
-
current_task?: string;
|
|
1897
|
-
blockers?: string[];
|
|
1898
|
-
progress_percent?: number;
|
|
1899
|
-
created_at?: string;
|
|
1900
|
-
},
|
|
1901
|
-
): Promise<{
|
|
1902
|
-
learningsExtracted: number;
|
|
1903
|
-
feedbackAdjusted: number;
|
|
1904
|
-
maintenance?: {
|
|
1905
|
-
archived: number;
|
|
1906
|
-
pruned: number;
|
|
1907
|
-
promoted: number;
|
|
1908
|
-
reviewed: number;
|
|
1909
|
-
};
|
|
1910
|
-
}> {
|
|
1911
|
-
let learningsExtracted = 0;
|
|
1912
|
-
let feedbackAdjusted = 0;
|
|
1913
|
-
let maintenanceResult:
|
|
1914
|
-
| { archived: number; pruned: number; promoted: number; reviewed: number }
|
|
1915
|
-
| undefined;
|
|
1916
|
-
|
|
1917
|
-
// Fetch card details for learning extraction
|
|
1918
|
-
let cardTitle = "";
|
|
1919
|
-
let cardLabels: string[] = [];
|
|
1920
|
-
let cardDescription = "";
|
|
1921
|
-
let cardSubtasks: Array<{ title: string; done: boolean }> = [];
|
|
1922
|
-
try {
|
|
1923
|
-
const { card } = await client.getCard(cardId);
|
|
1924
|
-
const typedCard = card as {
|
|
1925
|
-
title?: string;
|
|
1926
|
-
description?: string;
|
|
1927
|
-
labels?: Array<{ id: string; name: string }>;
|
|
1928
|
-
subtasks?: Array<{ title: string; done: boolean }>;
|
|
1929
|
-
};
|
|
1930
|
-
cardTitle = typedCard.title || "";
|
|
1931
|
-
cardLabels = (typedCard.labels || []).map((l) => l.name);
|
|
1932
|
-
cardDescription = typedCard.description || "";
|
|
1933
|
-
cardSubtasks = (typedCard.subtasks || []).map((s) => ({
|
|
1934
|
-
title: s.title,
|
|
1935
|
-
done: s.done,
|
|
1936
|
-
}));
|
|
1533
|
+
/**
|
|
1534
|
+
* Reusable end-session pipeline.
|
|
1535
|
+
* Called by both explicit harmony_end_agent_session and auto-session timeout/card-switch.
|
|
1536
|
+
*
|
|
1537
|
+
* Phase 0 (memory architecture v2): AGP-specific steps (active learning extraction,
|
|
1538
|
+
* recordContextFeedback, runLifecycleMaintenance, auto-consolidation) have been
|
|
1539
|
+
* removed. Only the agent label cleanup remains.
|
|
1540
|
+
* Feedback writes (last_accessed_at + confidence per §10.3 of
|
|
1541
|
+
* docs/superpowers/plans/2026-05-07-memory-architecture-v2.md) will be reintroduced
|
|
1542
|
+
* in Phase 2 when the rebuilt context-assembly pipeline lands.
|
|
1543
|
+
*/
|
|
1544
|
+
export async function runEndSessionPipeline(
|
|
1545
|
+
client: HarmonyApiClient,
|
|
1546
|
+
_deps: ToolDeps,
|
|
1547
|
+
cardId: string,
|
|
1548
|
+
sessionStatus: "completed" | "paused",
|
|
1549
|
+
): Promise<{
|
|
1550
|
+
learningsExtracted: number;
|
|
1551
|
+
feedbackAdjusted: number;
|
|
1552
|
+
}> {
|
|
1553
|
+
// Fetch card details so we can clean up the "agent" label on completion.
|
|
1554
|
+
try {
|
|
1555
|
+
const { card } = await client.getCard(cardId);
|
|
1556
|
+
const typedCard = card as {
|
|
1557
|
+
title?: string;
|
|
1558
|
+
description?: string;
|
|
1559
|
+
labels?: Array<{ id: string; name: string }>;
|
|
1560
|
+
subtasks?: Array<{ title: string; done: boolean }>;
|
|
1561
|
+
};
|
|
1937
1562
|
|
|
1938
1563
|
// Remove "agent" label when session is completed (not paused)
|
|
1939
1564
|
if (sessionStatus === "completed" && typedCard.labels?.length) {
|
|
@@ -1945,166 +1570,12 @@ export async function runEndSessionPipeline(
|
|
|
1945
1570
|
}
|
|
1946
1571
|
}
|
|
1947
1572
|
} catch {
|
|
1948
|
-
// Card fetch failed, continue
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
// Active learning: extract memories from session
|
|
1952
|
-
try {
|
|
1953
|
-
let sessionDurationMs: number | undefined;
|
|
1954
|
-
if (sessionData?.created_at) {
|
|
1955
|
-
const startTime = new Date(sessionData.created_at).getTime();
|
|
1956
|
-
if (!Number.isNaN(startTime)) {
|
|
1957
|
-
sessionDurationMs = Date.now() - startTime;
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
const sessionContext: SessionContext = {
|
|
1962
|
-
cardId,
|
|
1963
|
-
cardTitle,
|
|
1964
|
-
cardLabels,
|
|
1965
|
-
agentIdentifier: sessionData?.agent_identifier || "unknown",
|
|
1966
|
-
agentName: sessionData?.agent_name || "Unknown Agent",
|
|
1967
|
-
status: sessionStatus,
|
|
1968
|
-
progressPercent: endProgressPercent,
|
|
1969
|
-
blockers: sessionData?.blockers || undefined,
|
|
1970
|
-
currentTask: sessionData?.current_task || undefined,
|
|
1971
|
-
sessionDurationMs,
|
|
1972
|
-
cardDescription: cardDescription || undefined,
|
|
1973
|
-
cardSubtasks: cardSubtasks.length > 0 ? cardSubtasks : undefined,
|
|
1974
|
-
};
|
|
1975
|
-
|
|
1976
|
-
const learningResult = await extractLearnings(client, sessionContext);
|
|
1977
|
-
learningsExtracted = learningResult.count;
|
|
1978
|
-
} catch {
|
|
1979
|
-
// Learning extraction failed, non-fatal
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
// Agent performance profile: refresh materialized view + upsert knowledge entity
|
|
1983
|
-
try {
|
|
1984
|
-
const workspaceId = deps.getActiveWorkspaceId();
|
|
1985
|
-
const agentId = sessionData?.agent_identifier || "unknown";
|
|
1986
|
-
if (workspaceId && agentId !== "unknown") {
|
|
1987
|
-
// Fire-and-forget: refresh + upsert agent profile entity
|
|
1988
|
-
(async () => {
|
|
1989
|
-
try {
|
|
1990
|
-
await client.refreshAgentProfiles(workspaceId);
|
|
1991
|
-
const { profile } = await client.getAgentProfile(
|
|
1992
|
-
workspaceId,
|
|
1993
|
-
agentId,
|
|
1994
|
-
);
|
|
1995
|
-
if (profile) {
|
|
1996
|
-
const p = profile as Record<string, unknown>;
|
|
1997
|
-
const title = `Agent Profile: ${agentId}`;
|
|
1998
|
-
const content = [
|
|
1999
|
-
`## ${agentId} Performance Profile`,
|
|
2000
|
-
"",
|
|
2001
|
-
`- **Total sessions:** ${p.total_sessions}`,
|
|
2002
|
-
`- **Completed:** ${p.completed_sessions} (${p.completion_rate_pct}%)`,
|
|
2003
|
-
`- **Paused:** ${p.paused_sessions}`,
|
|
2004
|
-
`- **Blocked:** ${p.blocked_sessions}`,
|
|
2005
|
-
`- **Avg duration:** ${Math.round(Number(p.avg_active_duration_ms || 0) / 1000)}s`,
|
|
2006
|
-
`- **Avg progress:** ${p.avg_completion_progress}%`,
|
|
2007
|
-
`- **First session:** ${p.first_session_at}`,
|
|
2008
|
-
`- **Last session:** ${p.last_session_at}`,
|
|
2009
|
-
].join("\n");
|
|
2010
|
-
|
|
2011
|
-
// Find existing agent entity to update, or create new
|
|
2012
|
-
const existing = await client.listMemoryEntities({
|
|
2013
|
-
workspace_id: workspaceId,
|
|
2014
|
-
type: "agent",
|
|
2015
|
-
limit: 50,
|
|
2016
|
-
});
|
|
2017
|
-
const entities = (existing.entities || []) as Array<{
|
|
2018
|
-
id: string;
|
|
2019
|
-
title: string;
|
|
2020
|
-
agent_identifier?: string;
|
|
2021
|
-
}>;
|
|
2022
|
-
const match = entities.find(
|
|
2023
|
-
(e) => e.title === title || e.agent_identifier === agentId,
|
|
2024
|
-
);
|
|
2025
|
-
|
|
2026
|
-
if (match) {
|
|
2027
|
-
await client.updateMemoryEntity(match.id, {
|
|
2028
|
-
content,
|
|
2029
|
-
confidence: 1.0,
|
|
2030
|
-
metadata: p,
|
|
2031
|
-
});
|
|
2032
|
-
} else {
|
|
2033
|
-
await client.createMemoryEntity({
|
|
2034
|
-
workspace_id: workspaceId,
|
|
2035
|
-
type: "agent",
|
|
2036
|
-
scope: "workspace",
|
|
2037
|
-
memory_tier: "reference",
|
|
2038
|
-
title,
|
|
2039
|
-
content,
|
|
2040
|
-
confidence: 1.0,
|
|
2041
|
-
tags: ["agent-profile", agentId],
|
|
2042
|
-
agent_identifier: agentId,
|
|
2043
|
-
metadata: p,
|
|
2044
|
-
});
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
} catch {
|
|
2048
|
-
// Non-fatal: profile upsert failed
|
|
2049
|
-
}
|
|
2050
|
-
})();
|
|
2051
|
-
}
|
|
2052
|
-
} catch {
|
|
2053
|
-
// Non-fatal: agent profile refresh failed
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
// Feedback-driven scoring
|
|
2057
|
-
try {
|
|
2058
|
-
const feedbackResult = await recordContextFeedback(
|
|
2059
|
-
client,
|
|
2060
|
-
cardId,
|
|
2061
|
-
sessionStatus,
|
|
2062
|
-
endProgressPercent,
|
|
2063
|
-
(sessionData?.blockers?.length ?? 0) > 0,
|
|
2064
|
-
);
|
|
2065
|
-
feedbackAdjusted = feedbackResult.adjusted;
|
|
2066
|
-
} catch {
|
|
2067
|
-
// Feedback recording failed, non-fatal
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
// Lifecycle maintenance
|
|
2071
|
-
try {
|
|
2072
|
-
const workspaceId = deps.getActiveWorkspaceId();
|
|
2073
|
-
if (workspaceId) {
|
|
2074
|
-
const projectId = deps.getActiveProjectId() || undefined;
|
|
2075
|
-
|
|
2076
|
-
maintenanceResult = await runLifecycleMaintenance(
|
|
2077
|
-
client,
|
|
2078
|
-
workspaceId,
|
|
2079
|
-
projectId,
|
|
2080
|
-
);
|
|
2081
|
-
|
|
2082
|
-
// Auto-consolidation: if workspace has many draft/episode entities
|
|
2083
|
-
const listResult = await client.listMemoryEntities({
|
|
2084
|
-
workspace_id: workspaceId,
|
|
2085
|
-
project_id: projectId,
|
|
2086
|
-
limit: 100,
|
|
2087
|
-
});
|
|
2088
|
-
const draftEpisodeCount = (
|
|
2089
|
-
(listResult.entities || []) as Array<{ memory_tier?: string }>
|
|
2090
|
-
).filter(
|
|
2091
|
-
(e) => e.memory_tier === "draft" || e.memory_tier === "episode",
|
|
2092
|
-
).length;
|
|
2093
|
-
if (draftEpisodeCount > 50) {
|
|
2094
|
-
consolidateMemories(client, workspaceId, projectId, {
|
|
2095
|
-
dryRun: false,
|
|
2096
|
-
minClusterSize: 2,
|
|
2097
|
-
}).catch(() => {});
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
} catch {
|
|
2101
|
-
// Lifecycle maintenance failed, non-fatal
|
|
1573
|
+
// Card fetch failed, continue
|
|
2102
1574
|
}
|
|
2103
1575
|
|
|
2104
1576
|
return {
|
|
2105
|
-
learningsExtracted,
|
|
2106
|
-
feedbackAdjusted,
|
|
2107
|
-
...(maintenanceResult && { maintenance: maintenanceResult }),
|
|
1577
|
+
learningsExtracted: 0,
|
|
1578
|
+
feedbackAdjusted: 0,
|
|
2108
1579
|
};
|
|
2109
1580
|
}
|
|
2110
1581
|
|
|
@@ -2752,72 +2223,18 @@ async function handleToolCall(
|
|
|
2752
2223
|
// Mark as explicit so auto-session won't interfere
|
|
2753
2224
|
markExplicit(cardId, { agentIdentifier, agentName });
|
|
2754
2225
|
|
|
2755
|
-
// Initialize memory session tracking for action visibility
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
try {
|
|
2761
|
-
const workspaceId = deps.getActiveWorkspaceId();
|
|
2762
|
-
if (workspaceId) {
|
|
2763
|
-
// Get card details for context
|
|
2764
|
-
const { card } = await client.getCard(cardId);
|
|
2765
|
-
const typedCard = card as {
|
|
2766
|
-
title?: string;
|
|
2767
|
-
description?: string;
|
|
2768
|
-
labels?: Array<{ name: string }>;
|
|
2769
|
-
};
|
|
2770
|
-
const cardLabels = (typedCard.labels || []).map((l) => l.name);
|
|
2771
|
-
const taskContext = [
|
|
2772
|
-
typedCard.title || "",
|
|
2773
|
-
typedCard.description || "",
|
|
2774
|
-
]
|
|
2775
|
-
.filter(Boolean)
|
|
2776
|
-
.join(" ");
|
|
2777
|
-
|
|
2778
|
-
// Assemble context to find seed entities
|
|
2779
|
-
const assembled = await assembleContext({
|
|
2780
|
-
workspaceId,
|
|
2781
|
-
projectId: getActiveProjectId() || undefined,
|
|
2782
|
-
taskContext,
|
|
2783
|
-
cardLabels,
|
|
2784
|
-
cardId,
|
|
2785
|
-
tokenBudget: 2000, // Smaller budget for prefetch
|
|
2786
|
-
client,
|
|
2787
|
-
});
|
|
2788
|
-
|
|
2789
|
-
prefetchedMemoryIds = assembled.memories.map((m) => m.id);
|
|
2790
|
-
|
|
2791
|
-
// Track assembly for feedback loop
|
|
2792
|
-
if (assembled.manifest.assemblyId) {
|
|
2793
|
-
cacheManifest(assembled.manifest);
|
|
2794
|
-
trackSessionAssembly(cardId, assembled.manifest.assemblyId);
|
|
2795
|
-
}
|
|
2796
|
-
|
|
2797
|
-
// Walk graph from seed entities to discover more context
|
|
2798
|
-
if (prefetchedMemoryIds.length > 0) {
|
|
2799
|
-
const graphResult = await discoverRelatedContext(
|
|
2800
|
-
client,
|
|
2801
|
-
prefetchedMemoryIds.slice(0, 5), // Limit seeds
|
|
2802
|
-
2, // 2 hops
|
|
2803
|
-
10, // Max 10 entities
|
|
2804
|
-
);
|
|
2805
|
-
const additionalIds = graphResult.entities
|
|
2806
|
-
.map((e) => e.id)
|
|
2807
|
-
.filter((id) => !prefetchedMemoryIds.includes(id));
|
|
2808
|
-
prefetchedMemoryIds.push(...additionalIds);
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
} catch {
|
|
2812
|
-
// Prefetch failed, non-fatal
|
|
2813
|
-
}
|
|
2226
|
+
// Initialize memory session tracking for action visibility. Capture the
|
|
2227
|
+
// backend session id so working-memory writes (`scope: 'session'`) bind
|
|
2228
|
+
// to the same `card_agent_context` row that progress/end calls target.
|
|
2229
|
+
const agentSessionId = (result.session as { id?: string } | null)?.id;
|
|
2230
|
+
initMemorySession(cardId, agentIdentifier, agentName, agentSessionId);
|
|
2814
2231
|
|
|
2815
2232
|
return {
|
|
2816
2233
|
success: true,
|
|
2817
2234
|
assignedTo,
|
|
2818
2235
|
movedTo,
|
|
2819
2236
|
labelsAdded,
|
|
2820
|
-
prefetchedMemoryCount:
|
|
2237
|
+
prefetchedMemoryCount: 0,
|
|
2821
2238
|
...result,
|
|
2822
2239
|
};
|
|
2823
2240
|
}
|
|
@@ -2875,34 +2292,8 @@ async function handleToolCall(
|
|
|
2875
2292
|
...(mergedRecentActions && { recentActions: mergedRecentActions }),
|
|
2876
2293
|
});
|
|
2877
2294
|
|
|
2878
|
-
//
|
|
2879
|
-
|
|
2880
|
-
try {
|
|
2881
|
-
const { card } = await client.getCard(cardId);
|
|
2882
|
-
const typedCard = card as {
|
|
2883
|
-
title?: string;
|
|
2884
|
-
};
|
|
2885
|
-
const midResult = await extractMidSessionLearnings(client, {
|
|
2886
|
-
cardId,
|
|
2887
|
-
cardTitle: typedCard.title || "",
|
|
2888
|
-
agentIdentifier,
|
|
2889
|
-
agentName,
|
|
2890
|
-
currentTask: args.currentTask as string | undefined,
|
|
2891
|
-
status: args.status as
|
|
2892
|
-
| "working"
|
|
2893
|
-
| "blocked"
|
|
2894
|
-
| "waiting"
|
|
2895
|
-
| "paused"
|
|
2896
|
-
| undefined,
|
|
2897
|
-
blockers: args.blockers as string[] | undefined,
|
|
2898
|
-
progressPercent,
|
|
2899
|
-
});
|
|
2900
|
-
midSessionLearnings = midResult.count;
|
|
2901
|
-
} catch {
|
|
2902
|
-
// Non-fatal: mid-session learning extraction failure
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
|
-
return { success: true, midSessionLearnings, ...result };
|
|
2295
|
+
// Phase 0 (memory architecture v2): mid-session learning extraction removed.
|
|
2296
|
+
return { success: true, midSessionLearnings: 0, ...result };
|
|
2906
2297
|
}
|
|
2907
2298
|
|
|
2908
2299
|
case "harmony_end_agent_session": {
|
|
@@ -2969,21 +2360,11 @@ async function handleToolCall(
|
|
|
2969
2360
|
}
|
|
2970
2361
|
|
|
2971
2362
|
// Run shared end-session pipeline (learning, feedback, maintenance)
|
|
2972
|
-
const sessionObj = result.session as {
|
|
2973
|
-
agent_identifier?: string;
|
|
2974
|
-
agent_name?: string;
|
|
2975
|
-
current_task?: string;
|
|
2976
|
-
blockers?: string[];
|
|
2977
|
-
progress_percent?: number;
|
|
2978
|
-
created_at?: string;
|
|
2979
|
-
};
|
|
2980
2363
|
const pipelineResult = await runEndSessionPipeline(
|
|
2981
2364
|
client,
|
|
2982
2365
|
deps,
|
|
2983
2366
|
cardId,
|
|
2984
2367
|
sessionStatus,
|
|
2985
|
-
endProgressPercent,
|
|
2986
|
-
sessionObj,
|
|
2987
2368
|
);
|
|
2988
2369
|
|
|
2989
2370
|
return {
|
|
@@ -2992,12 +2373,6 @@ async function handleToolCall(
|
|
|
2992
2373
|
movedTo,
|
|
2993
2374
|
learningsExtracted: pipelineResult.learningsExtracted,
|
|
2994
2375
|
feedbackAdjusted: pipelineResult.feedbackAdjusted,
|
|
2995
|
-
...(pipelineResult.maintenance &&
|
|
2996
|
-
(pipelineResult.maintenance.archived > 0 ||
|
|
2997
|
-
pipelineResult.maintenance.pruned > 0 ||
|
|
2998
|
-
pipelineResult.maintenance.promoted > 0) && {
|
|
2999
|
-
maintenance: pipelineResult.maintenance,
|
|
3000
|
-
}),
|
|
3001
2376
|
...result,
|
|
3002
2377
|
};
|
|
3003
2378
|
}
|
|
@@ -3011,21 +2386,8 @@ async function handleToolCall(
|
|
|
3011
2386
|
return { success: true, ...result };
|
|
3012
2387
|
}
|
|
3013
2388
|
|
|
3014
|
-
case "harmony_get_agent_profile": {
|
|
3015
|
-
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
|
3016
|
-
if (!workspaceId) {
|
|
3017
|
-
return {
|
|
3018
|
-
success: false,
|
|
3019
|
-
error: "No workspace context. Provide workspaceId or set context.",
|
|
3020
|
-
};
|
|
3021
|
-
}
|
|
3022
|
-
const agentIdentifier = args.agentIdentifier || "claude-code";
|
|
3023
|
-
|
|
3024
|
-
const result = await client.getAgentProfile(workspaceId, agentIdentifier);
|
|
3025
|
-
return { success: true, ...result };
|
|
3026
|
-
}
|
|
3027
|
-
|
|
3028
2389
|
// Prompt generation
|
|
2390
|
+
// TODO Phase 1: rebuild full context assembly per docs/superpowers/plans/2026-05-07-memory-architecture-v2.md §10
|
|
3029
2391
|
case "harmony_generate_prompt": {
|
|
3030
2392
|
// Resolve card ID — either directly or via short ID
|
|
3031
2393
|
let cardId: string;
|
|
@@ -3047,41 +2409,28 @@ async function handleToolCall(
|
|
|
3047
2409
|
throw new Error("Either cardId or shortId must be provided");
|
|
3048
2410
|
}
|
|
3049
2411
|
|
|
3050
|
-
//
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
contextOptions.includeDescription =
|
|
3062
|
-
args.includeDescription === true ||
|
|
3063
|
-
args.includeDescription === "true";
|
|
2412
|
+
// Phase 0: minimal placeholder prompt with no context assembly.
|
|
2413
|
+
// Fetch just the card title/description so the agent has the basics.
|
|
2414
|
+
let cardTitle = "";
|
|
2415
|
+
let cardDescription = "";
|
|
2416
|
+
try {
|
|
2417
|
+
const { card } = await client.getCard(cardId);
|
|
2418
|
+
const typedCard = card as { title?: string; description?: string };
|
|
2419
|
+
cardTitle = typedCard.title || "";
|
|
2420
|
+
cardDescription = typedCard.description || "";
|
|
2421
|
+
} catch {
|
|
2422
|
+
// Card fetch failed; return an empty task description.
|
|
3064
2423
|
}
|
|
3065
2424
|
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
projectId:
|
|
3071
|
-
(args.projectId as string) || getActiveProjectId() || undefined,
|
|
3072
|
-
variant: (args.variant as PromptVariant) || "execute",
|
|
3073
|
-
customConstraints: args.customConstraints as string | undefined,
|
|
3074
|
-
contextOptions,
|
|
3075
|
-
});
|
|
3076
|
-
|
|
3077
|
-
// MCP-specific: cache the assembly manifest for the feedback loop
|
|
3078
|
-
if (result.assemblyId) {
|
|
3079
|
-
trackSessionAssembly(cardId, result.assemblyId);
|
|
3080
|
-
}
|
|
2425
|
+
const taskBlock = [cardTitle, cardDescription]
|
|
2426
|
+
.filter(Boolean)
|
|
2427
|
+
.join("\n\n");
|
|
2428
|
+
const prompt = `Here is the task: ${taskBlock || "(no description available)"}`;
|
|
3081
2429
|
|
|
3082
2430
|
return {
|
|
3083
2431
|
success: true,
|
|
3084
|
-
|
|
2432
|
+
prompt,
|
|
2433
|
+
cardId,
|
|
3085
2434
|
};
|
|
3086
2435
|
}
|
|
3087
2436
|
|
|
@@ -3098,14 +2447,52 @@ async function handleToolCall(
|
|
|
3098
2447
|
}
|
|
3099
2448
|
const entityType = (args.type as string) || "context";
|
|
3100
2449
|
const entityTags = (args.tags as string[]) || [];
|
|
3101
|
-
|
|
2450
|
+
|
|
2451
|
+
// Working-memory binding (plan §12 Phase 1, §13.1 D3). The caller may
|
|
2452
|
+
// pass `scope: 'session'` as an alias; resolve it to the active
|
|
2453
|
+
// `card_agent_context` id so the row carries `session:<uuid>` on disk.
|
|
2454
|
+
// Other scope values pass through unchanged.
|
|
3102
2455
|
const activeMemSession = getActiveMemorySession();
|
|
2456
|
+
const entityScope =
|
|
2457
|
+
resolveSessionScope(
|
|
2458
|
+
args.scope as string | undefined,
|
|
2459
|
+
activeMemSession?.agentSessionId,
|
|
2460
|
+
) || "project";
|
|
2461
|
+
|
|
2462
|
+
// Utility Floor (plan §4.5.1) — write admission control.
|
|
2463
|
+
// Rejects tag-concat slop, frequency-meta titles, self-referential
|
|
2464
|
+
// operational notes, bare type prefixes, under-specific titles, and
|
|
2465
|
+
// operational data dumps. Session-scope memories are exempt.
|
|
2466
|
+
const floorRejection = validateMemoryQuality({
|
|
2467
|
+
title,
|
|
2468
|
+
content,
|
|
2469
|
+
type: entityType,
|
|
2470
|
+
scope: entityScope,
|
|
2471
|
+
source_trust: args.source_trust as string | undefined,
|
|
2472
|
+
tags: entityTags,
|
|
2473
|
+
});
|
|
2474
|
+
if (floorRejection) {
|
|
2475
|
+
throw new Error(
|
|
2476
|
+
`Memory rejected by Utility Floor [${floorRejection.rule}]: ${floorRejection.message}`,
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
// Importance: caller-provided > per-type default (plan §7.1).
|
|
2481
|
+
// auto_score_importance is documented as an LLM-rating opt-in but the
|
|
2482
|
+
// actual LLM call is deferred to a follow-up commit; currently it falls
|
|
2483
|
+
// through to the type default, same as omission.
|
|
2484
|
+
const callerImportance =
|
|
2485
|
+
args.importance !== undefined
|
|
2486
|
+
? z.number().int().min(1).max(10).parse(args.importance)
|
|
2487
|
+
: undefined;
|
|
2488
|
+
|
|
2489
|
+
// Use session's agent identifier if available, otherwise null
|
|
3103
2490
|
const result = await client.createMemoryEntity({
|
|
3104
2491
|
workspace_id: workspaceId,
|
|
3105
2492
|
project_id:
|
|
3106
2493
|
(args.projectId as string) || deps.getActiveProjectId() || undefined,
|
|
3107
2494
|
type: entityType,
|
|
3108
|
-
scope:
|
|
2495
|
+
scope: entityScope,
|
|
3109
2496
|
memory_tier: (args.tier as string) || undefined,
|
|
3110
2497
|
title,
|
|
3111
2498
|
content,
|
|
@@ -3114,6 +2501,7 @@ async function handleToolCall(
|
|
|
3114
2501
|
args.confidence !== undefined
|
|
3115
2502
|
? z.number().min(0).max(1).parse(args.confidence)
|
|
3116
2503
|
: undefined,
|
|
2504
|
+
importance: callerImportance,
|
|
3117
2505
|
tags: entityTags.length > 0 ? entityTags : undefined,
|
|
3118
2506
|
agent_identifier: activeMemSession?.agentIdentifier || undefined,
|
|
3119
2507
|
});
|
|
@@ -3132,32 +2520,8 @@ async function handleToolCall(
|
|
|
3132
2520
|
).catch(() => {});
|
|
3133
2521
|
}
|
|
3134
2522
|
|
|
3135
|
-
//
|
|
3136
|
-
|
|
3137
|
-
entityId: string;
|
|
3138
|
-
title: string;
|
|
3139
|
-
type: string;
|
|
3140
|
-
tags: string[];
|
|
3141
|
-
}> = [];
|
|
3142
|
-
const newEntityId = (result.entity as any)?.id;
|
|
3143
|
-
if (newEntityId) {
|
|
3144
|
-
try {
|
|
3145
|
-
potentialContradictions = await detectContradictions(
|
|
3146
|
-
client,
|
|
3147
|
-
newEntityId,
|
|
3148
|
-
entityType,
|
|
3149
|
-
title,
|
|
3150
|
-
content,
|
|
3151
|
-
entityTags,
|
|
3152
|
-
workspaceId,
|
|
3153
|
-
(args.projectId as string) ||
|
|
3154
|
-
deps.getActiveProjectId() ||
|
|
3155
|
-
undefined,
|
|
3156
|
-
);
|
|
3157
|
-
} catch {
|
|
3158
|
-
// Don't block creation if contradiction detection fails
|
|
3159
|
-
}
|
|
3160
|
-
}
|
|
2523
|
+
// Phase 0 (memory architecture v2): semantic contradiction detection removed
|
|
2524
|
+
// along with the active-learning module. Will be revisited in Phase 2.
|
|
3161
2525
|
|
|
3162
2526
|
// Track memory write action and flush (fire-and-forget)
|
|
3163
2527
|
if (activeMemSession) {
|
|
@@ -3168,14 +2532,6 @@ async function handleToolCall(
|
|
|
3168
2532
|
return {
|
|
3169
2533
|
success: true,
|
|
3170
2534
|
...result,
|
|
3171
|
-
...(potentialContradictions.length > 0 && {
|
|
3172
|
-
potentialContradictions: potentialContradictions.map((c) => ({
|
|
3173
|
-
id: c.entityId,
|
|
3174
|
-
title: c.title,
|
|
3175
|
-
tags: c.tags,
|
|
3176
|
-
})),
|
|
3177
|
-
contradictionNote: `Found ${potentialContradictions.length} semantically similar memor${potentialContradictions.length === 1 ? "y" : "ies"} of same type that may contradict. 'contradicts' relations created automatically. Review these to resolve or confirm.`,
|
|
3178
|
-
}),
|
|
3179
2535
|
};
|
|
3180
2536
|
}
|
|
3181
2537
|
|
|
@@ -3187,74 +2543,212 @@ async function handleToolCall(
|
|
|
3187
2543
|
"No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
|
|
3188
2544
|
);
|
|
3189
2545
|
}
|
|
3190
|
-
const
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
2546
|
+
const requestedLimit = args.limit as number | undefined;
|
|
2547
|
+
const topK = args.top_k as number | undefined;
|
|
2548
|
+
const budgetTokens = args.budget_tokens as number | undefined;
|
|
2549
|
+
const includeSuperseded = Boolean(args.include_superseded);
|
|
2550
|
+
const queryText = args.query as string | undefined;
|
|
2551
|
+
|
|
2552
|
+
// Over-fetch when top_k is smaller than limit so the rescorer has
|
|
2553
|
+
// more candidates to choose from. Cap at 200 to keep the round-trip
|
|
2554
|
+
// bounded.
|
|
2555
|
+
const fetchLimit = Math.min(
|
|
2556
|
+
Math.max(requestedLimit ?? 20, topK ?? 0, 50),
|
|
2557
|
+
200,
|
|
2558
|
+
);
|
|
2559
|
+
|
|
2560
|
+
const projectId =
|
|
2561
|
+
(args.projectId as string) || deps.getActiveProjectId() || undefined;
|
|
2562
|
+
|
|
2563
|
+
// Resolve the working-memory alias before anything else: callers pass
|
|
2564
|
+
// `scope: 'session'` and we bind it to the active session id. When the
|
|
2565
|
+
// scope was implicit (undefined) we will instead prepend session
|
|
2566
|
+
// memories on top of the long-term result below.
|
|
2567
|
+
const recallMemSession = getActiveMemorySession();
|
|
2568
|
+
const explicitScope = resolveSessionScope(
|
|
2569
|
+
args.scope as string | undefined,
|
|
2570
|
+
recallMemSession?.agentSessionId,
|
|
2571
|
+
);
|
|
2572
|
+
|
|
2573
|
+
// When the caller asked for a specific scope (resolved or otherwise),
|
|
2574
|
+
// honor it exactly. Without a caller scope we default to "all
|
|
2575
|
+
// long-term scopes" — i.e. exclude any working-memory rows from the
|
|
2576
|
+
// long-term mix because they get prepended separately below.
|
|
2577
|
+
const userScopeFilter = explicitScope;
|
|
2578
|
+
const excludeSessionFromLongTerm = explicitScope === undefined;
|
|
2579
|
+
|
|
2580
|
+
// Hybrid retrieval (plan §6.2): when a free-text query is provided,
|
|
2581
|
+
// route through /memory/search which fuses pgvector cosine + Postgres
|
|
2582
|
+
// FTS via the hybrid_search_knowledge_entities RPC (RRF, k=50). The
|
|
2583
|
+
// returned ranking becomes the relevance signal feeding Park's
|
|
2584
|
+
// rescorer. When no query is given, fall back to filter-only listing
|
|
2585
|
+
// with neutral 0.5 relevance — recency + importance still differentiate.
|
|
2586
|
+
let entities: any[];
|
|
2587
|
+
let relevanceMap: Map<string, number>;
|
|
2588
|
+
|
|
2589
|
+
if (queryText) {
|
|
2590
|
+
const searchResult = await client.searchMemoryEntities(
|
|
2591
|
+
workspaceId,
|
|
2592
|
+
queryText,
|
|
2593
|
+
{
|
|
2594
|
+
project_id: projectId,
|
|
2595
|
+
type: args.type as string | undefined,
|
|
2596
|
+
limit: fetchLimit,
|
|
2597
|
+
},
|
|
2598
|
+
);
|
|
2599
|
+
entities = (searchResult.entities ?? []) as any[];
|
|
2600
|
+
|
|
2601
|
+
// Post-filter the rest of the params client-side. Hybrid search RPC
|
|
2602
|
+
// exposes only project_id + type; tags / scope / minConfidence /
|
|
2603
|
+
// include_superseded are applied here on the rank-ordered set.
|
|
2604
|
+
const requestedTags = args.tags as string[] | undefined;
|
|
2605
|
+
const minConfidence = args.minConfidence as number | undefined;
|
|
2606
|
+
if (userScopeFilter) {
|
|
2607
|
+
entities = entities.filter((e) => e?.scope === userScopeFilter);
|
|
2608
|
+
} else if (excludeSessionFromLongTerm) {
|
|
2609
|
+
// Working-memory rows are surfaced via the prepend pass; keep them
|
|
2610
|
+
// out of the long-term mix so the same row never shows twice.
|
|
2611
|
+
entities = entities.filter((e) => !isSessionScope(e?.scope));
|
|
2612
|
+
}
|
|
2613
|
+
if (requestedTags && requestedTags.length > 0) {
|
|
2614
|
+
const wanted = new Set(requestedTags);
|
|
2615
|
+
entities = entities.filter((e) =>
|
|
2616
|
+
(e?.tags ?? []).some((t: string) => wanted.has(t)),
|
|
2617
|
+
);
|
|
2618
|
+
}
|
|
2619
|
+
if (typeof minConfidence === "number") {
|
|
2620
|
+
entities = entities.filter(
|
|
2621
|
+
(e) =>
|
|
2622
|
+
typeof e?.confidence === "number" &&
|
|
2623
|
+
e.confidence >= minConfidence,
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
if (!includeSuperseded) {
|
|
2627
|
+
entities = entities.filter((e) => !e?.superseded_at);
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
relevanceMap = relevanceFromRank(entities as Array<{ id: string }>);
|
|
2631
|
+
} else {
|
|
2632
|
+
const { entities: listed } = await client.listMemoryEntities({
|
|
2633
|
+
workspace_id: workspaceId,
|
|
2634
|
+
project_id: projectId,
|
|
2635
|
+
type: args.type as string | undefined,
|
|
2636
|
+
scope: userScopeFilter,
|
|
2637
|
+
tags: args.tags as string[] | undefined,
|
|
2638
|
+
min_confidence: args.minConfidence as number | undefined,
|
|
2639
|
+
limit: fetchLimit,
|
|
2640
|
+
include_superseded: includeSuperseded,
|
|
2641
|
+
});
|
|
2642
|
+
entities = listed as any[];
|
|
2643
|
+
if (excludeSessionFromLongTerm) {
|
|
2644
|
+
// Match the with-query branch: prevent working memory from
|
|
2645
|
+
// double-appearing once we prepend the session pool.
|
|
2646
|
+
entities = entities.filter((e) => !isSessionScope(e?.scope));
|
|
2647
|
+
}
|
|
2648
|
+
relevanceMap = new Map(); // neutral baseline; rescore uses 0.5
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// Park rescore (plan §6.2 step 4). When the hybrid path supplied a
|
|
2652
|
+
// relevance map, top-ranked entities dominate the relevance term;
|
|
2653
|
+
// otherwise the neutral 0.5 baseline lets recency + importance
|
|
2654
|
+
// differentiate.
|
|
2655
|
+
const scored = rescore(entities as ParkInput[], {
|
|
2656
|
+
relevance: relevanceMap,
|
|
2657
|
+
});
|
|
3201
2658
|
|
|
3202
|
-
//
|
|
3203
|
-
const
|
|
3204
|
-
|
|
3205
|
-
client.listMemoryEntities(queryOpts),
|
|
3206
|
-
]);
|
|
2659
|
+
// Trim to top_k (or the original limit if top_k was not provided).
|
|
2660
|
+
const finalCount = topK ?? requestedLimit ?? 20;
|
|
2661
|
+
let trimmed = scored.slice(0, finalCount);
|
|
3207
2662
|
|
|
3208
|
-
//
|
|
3209
|
-
if (
|
|
2663
|
+
// Token budget pass.
|
|
2664
|
+
if (typeof budgetTokens === "number" && budgetTokens > 0) {
|
|
2665
|
+
trimmed = fitToBudget(trimmed, budgetTokens);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// Touch access timestamps for the entities we returned (basic Park
|
|
2669
|
+
// recency trail). Non-blocking.
|
|
2670
|
+
if (trimmed.length > 0) {
|
|
3210
2671
|
Promise.all(
|
|
3211
|
-
|
|
3212
|
-
if (!entity.memory_tier || !entity.created_at) return;
|
|
2672
|
+
trimmed.map(async ({ entity }: { entity: any }) => {
|
|
3213
2673
|
try {
|
|
3214
|
-
// Touch access count via API (triggers server-side touch_knowledge_entity)
|
|
3215
2674
|
await client.updateMemoryEntity(entity.id, {
|
|
3216
2675
|
metadata: {
|
|
3217
2676
|
...(entity.metadata || {}),
|
|
3218
2677
|
_last_recall: new Date().toISOString(),
|
|
3219
2678
|
},
|
|
3220
2679
|
});
|
|
3221
|
-
|
|
3222
|
-
const lifecycle = evaluateLifecycle({
|
|
3223
|
-
memory_tier: entity.memory_tier,
|
|
3224
|
-
confidence: entity.confidence ?? 1.0,
|
|
3225
|
-
access_count: entity.access_count ?? 0,
|
|
3226
|
-
last_accessed_at: entity.last_accessed_at,
|
|
3227
|
-
created_at: entity.created_at,
|
|
3228
|
-
});
|
|
3229
|
-
|
|
3230
|
-
if (
|
|
3231
|
-
lifecycle.promotion.eligible &&
|
|
3232
|
-
lifecycle.promotion.targetTier
|
|
3233
|
-
) {
|
|
3234
|
-
await client.updateMemoryEntity(entity.id, {
|
|
3235
|
-
memory_tier: lifecycle.promotion.targetTier,
|
|
3236
|
-
metadata: {
|
|
3237
|
-
...(entity.metadata || {}),
|
|
3238
|
-
promoted_at: new Date().toISOString(),
|
|
3239
|
-
promotion_reason: lifecycle.promotion.reason,
|
|
3240
|
-
},
|
|
3241
|
-
});
|
|
3242
|
-
}
|
|
3243
2680
|
} catch (_) {
|
|
3244
|
-
// Non-critical
|
|
2681
|
+
// Non-critical
|
|
3245
2682
|
}
|
|
3246
2683
|
}),
|
|
3247
2684
|
).catch(() => {});
|
|
3248
2685
|
}
|
|
3249
2686
|
|
|
2687
|
+
// Working-memory prepend (plan §12 Phase 1). When a session is active
|
|
2688
|
+
// and the caller did not pin a scope, fetch session-scoped memories for
|
|
2689
|
+
// the current `card_agent_context` row and surface them ahead of the
|
|
2690
|
+
// long-term Park-rescored set. Skipped when the caller explicitly
|
|
2691
|
+
// asked for any other scope so the result honors the request.
|
|
2692
|
+
let sessionEntities: any[] = [];
|
|
2693
|
+
if (excludeSessionFromLongTerm && recallMemSession?.agentSessionId) {
|
|
2694
|
+
try {
|
|
2695
|
+
const { entities: sessionListed } = await client.listMemoryEntities({
|
|
2696
|
+
workspace_id: workspaceId,
|
|
2697
|
+
project_id: projectId,
|
|
2698
|
+
scope: sessionScopeFor(recallMemSession.agentSessionId),
|
|
2699
|
+
limit: 50,
|
|
2700
|
+
include_superseded: includeSuperseded,
|
|
2701
|
+
});
|
|
2702
|
+
sessionEntities = (sessionListed ?? []) as any[];
|
|
2703
|
+
} catch (_) {
|
|
2704
|
+
// Session pool is best-effort; long-term recall must still succeed.
|
|
2705
|
+
sessionEntities = [];
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
|
|
3250
2709
|
// Track memory read (batched on flush)
|
|
3251
|
-
const recallMemSession = getActiveMemorySession();
|
|
3252
2710
|
if (recallMemSession) {
|
|
3253
2711
|
incrementMemoryReads(recallMemSession.cardId);
|
|
3254
2712
|
}
|
|
3255
2713
|
|
|
3256
|
-
|
|
3257
|
-
|
|
2714
|
+
if (trimmed.length === 0 && sessionEntities.length === 0) {
|
|
2715
|
+
return "No memories found.";
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// Render rescored entities as markdown, including the score breakdown
|
|
2719
|
+
// for debuggability. Session memories render first under their own
|
|
2720
|
+
// heading so consumers can tell working memory from long-term recall.
|
|
2721
|
+
const lines: string[] = [];
|
|
2722
|
+
|
|
2723
|
+
if (sessionEntities.length > 0) {
|
|
2724
|
+
lines.push("## Working memory (session)\n");
|
|
2725
|
+
for (const e of sessionEntities) {
|
|
2726
|
+
const tagStr = e.tags?.length ? ` \`${e.tags.join(", ")}\`` : "";
|
|
2727
|
+
lines.push(
|
|
2728
|
+
`### ${e.title}\n` +
|
|
2729
|
+
`- **type:** ${e.type} · **scope:** ${e.scope} · **tier:** ${e.memory_tier}${tagStr}\n\n` +
|
|
2730
|
+
`${(e.content ?? "").slice(0, 400)}${(e.content ?? "").length > 400 ? "…" : ""}\n`,
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
if (trimmed.length > 0) {
|
|
2736
|
+
if (sessionEntities.length > 0) {
|
|
2737
|
+
lines.push("## Long-term memory\n");
|
|
2738
|
+
}
|
|
2739
|
+
for (const s of trimmed) {
|
|
2740
|
+
const e = s.entity as any;
|
|
2741
|
+
const tagStr = e.tags?.length ? ` \`${e.tags.join(", ")}\`` : "";
|
|
2742
|
+
lines.push(
|
|
2743
|
+
`### ${e.title}\n` +
|
|
2744
|
+
`- **type:** ${e.type} · **scope:** ${e.scope} · **tier:** ${e.memory_tier}\n` +
|
|
2745
|
+
`- **score:** ${s.score.toFixed(3)} (relevance ${s.relevance.toFixed(2)}, recency ${s.recency.toFixed(2)}, importance ${s.importance.toFixed(2)})${tagStr}\n\n` +
|
|
2746
|
+
`${(e.content ?? "").slice(0, 400)}${(e.content ?? "").length > 400 ? "…" : ""}\n`,
|
|
2747
|
+
);
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
return lines.join("\n---\n");
|
|
2751
|
+
}
|
|
3258
2752
|
|
|
3259
2753
|
case "harmony_update_memory": {
|
|
3260
2754
|
const entityId = z.string().uuid().parse(args.entityId);
|
|
@@ -3619,16 +3113,7 @@ async function handleToolCall(
|
|
|
3619
3113
|
workspaceId,
|
|
3620
3114
|
plan.project_id,
|
|
3621
3115
|
).catch(() => {});
|
|
3622
|
-
|
|
3623
|
-
client,
|
|
3624
|
-
results.memoryEntityId,
|
|
3625
|
-
"lesson",
|
|
3626
|
-
title,
|
|
3627
|
-
summary,
|
|
3628
|
-
tags,
|
|
3629
|
-
workspaceId,
|
|
3630
|
-
plan.project_id,
|
|
3631
|
-
).catch(() => {});
|
|
3116
|
+
// Phase 0 (memory architecture v2): contradiction detection removed.
|
|
3632
3117
|
}
|
|
3633
3118
|
} catch (_) {
|
|
3634
3119
|
/* best-effort */
|
|
@@ -3642,400 +3127,6 @@ async function handleToolCall(
|
|
|
3642
3127
|
};
|
|
3643
3128
|
}
|
|
3644
3129
|
|
|
3645
|
-
case "harmony_debug_context": {
|
|
3646
|
-
const entityId = z.string().uuid().parse(args.entityId);
|
|
3647
|
-
const taskContext = z.string().min(1).max(2000).parse(args.taskContext);
|
|
3648
|
-
const cardLabels = (args.cardLabels as string[]) || [];
|
|
3649
|
-
|
|
3650
|
-
// Fetch the entity
|
|
3651
|
-
const entityResult = await client.getMemoryEntity(entityId);
|
|
3652
|
-
const entity = mapToContextEntity(entityResult.entity);
|
|
3653
|
-
|
|
3654
|
-
// Compute relevance score
|
|
3655
|
-
const { score, reasons } = computeRelevanceScore(
|
|
3656
|
-
entity,
|
|
3657
|
-
taskContext,
|
|
3658
|
-
cardLabels,
|
|
3659
|
-
);
|
|
3660
|
-
|
|
3661
|
-
// Compute lifecycle status
|
|
3662
|
-
const lifecycle = evaluateLifecycle({
|
|
3663
|
-
memory_tier: entity.memory_tier as "draft" | "episode" | "reference",
|
|
3664
|
-
confidence: entity.confidence,
|
|
3665
|
-
access_count: entity.access_count,
|
|
3666
|
-
last_accessed_at: entity.last_accessed_at,
|
|
3667
|
-
created_at: entity.updated_at, // using updated_at as proxy
|
|
3668
|
-
});
|
|
3669
|
-
|
|
3670
|
-
return {
|
|
3671
|
-
success: true,
|
|
3672
|
-
entity: {
|
|
3673
|
-
id: entity.id,
|
|
3674
|
-
title: entity.title,
|
|
3675
|
-
type: entity.type,
|
|
3676
|
-
tier: entity.memory_tier,
|
|
3677
|
-
confidence: entity.confidence,
|
|
3678
|
-
accessCount: entity.access_count,
|
|
3679
|
-
},
|
|
3680
|
-
relevance: {
|
|
3681
|
-
score: Math.round(score * 1000) / 1000,
|
|
3682
|
-
reasons,
|
|
3683
|
-
wouldBeIncluded: score >= 0.1,
|
|
3684
|
-
threshold: 0.1,
|
|
3685
|
-
},
|
|
3686
|
-
lifecycle: {
|
|
3687
|
-
decayScore: Math.round(lifecycle.decay.score * 1000) / 1000,
|
|
3688
|
-
daysSinceAccess: Math.round(lifecycle.decay.daysSinceAccess),
|
|
3689
|
-
promotionEligible: lifecycle.promotion.eligible,
|
|
3690
|
-
promotionTarget: lifecycle.promotion.targetTier,
|
|
3691
|
-
shouldArchive: lifecycle.shouldArchive,
|
|
3692
|
-
shouldFlagForReview: lifecycle.shouldFlagForReview,
|
|
3693
|
-
},
|
|
3694
|
-
suggestions: [
|
|
3695
|
-
...(lifecycle.promotion.eligible
|
|
3696
|
-
? [
|
|
3697
|
-
`Eligible for promotion to ${lifecycle.promotion.targetTier}: ${lifecycle.promotion.reason}`,
|
|
3698
|
-
]
|
|
3699
|
-
: []),
|
|
3700
|
-
...(lifecycle.shouldArchive
|
|
3701
|
-
? [`Consider archiving: ${lifecycle.archiveReason}`]
|
|
3702
|
-
: []),
|
|
3703
|
-
...(lifecycle.shouldFlagForReview
|
|
3704
|
-
? [`Flagged for review: ${lifecycle.reviewReason}`]
|
|
3705
|
-
: []),
|
|
3706
|
-
...(score < 0.1
|
|
3707
|
-
? [
|
|
3708
|
-
"Below relevance threshold - consider adding more specific tags or updating content",
|
|
3709
|
-
]
|
|
3710
|
-
: []),
|
|
3711
|
-
],
|
|
3712
|
-
};
|
|
3713
|
-
}
|
|
3714
|
-
|
|
3715
|
-
case "harmony_export_memory_graph": {
|
|
3716
|
-
const workspaceId =
|
|
3717
|
-
(args.workspaceId as string) || deps.getActiveWorkspaceId();
|
|
3718
|
-
if (!workspaceId) {
|
|
3719
|
-
throw new Error(
|
|
3720
|
-
"No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
|
|
3721
|
-
);
|
|
3722
|
-
}
|
|
3723
|
-
const projectId =
|
|
3724
|
-
(args.projectId as string) || deps.getActiveProjectId() || undefined;
|
|
3725
|
-
const limit = (args.limit as number) || 50;
|
|
3726
|
-
|
|
3727
|
-
// Fetch entities
|
|
3728
|
-
const result = await client.listMemoryEntities({
|
|
3729
|
-
workspace_id: workspaceId,
|
|
3730
|
-
project_id: projectId,
|
|
3731
|
-
limit,
|
|
3732
|
-
});
|
|
3733
|
-
|
|
3734
|
-
const entities = (result.entities || []) as Array<{
|
|
3735
|
-
id: string;
|
|
3736
|
-
title: string;
|
|
3737
|
-
type: string;
|
|
3738
|
-
memory_tier?: string;
|
|
3739
|
-
confidence: number;
|
|
3740
|
-
}>;
|
|
3741
|
-
|
|
3742
|
-
// Build DOT graph
|
|
3743
|
-
const tierColors: Record<string, string> = {
|
|
3744
|
-
draft: "#FFEB3B", // yellow
|
|
3745
|
-
episode: "#2196F3", // blue
|
|
3746
|
-
reference: "#4CAF50", // green
|
|
3747
|
-
};
|
|
3748
|
-
|
|
3749
|
-
const typeShapes: Record<string, string> = {
|
|
3750
|
-
error: "octagon",
|
|
3751
|
-
solution: "diamond",
|
|
3752
|
-
pattern: "hexagon",
|
|
3753
|
-
decision: "house",
|
|
3754
|
-
lesson: "ellipse",
|
|
3755
|
-
preference: "parallelogram",
|
|
3756
|
-
};
|
|
3757
|
-
|
|
3758
|
-
const lines: string[] = [
|
|
3759
|
-
"digraph KnowledgeGraph {",
|
|
3760
|
-
" rankdir=LR;",
|
|
3761
|
-
" node [style=filled, fontsize=10];",
|
|
3762
|
-
"",
|
|
3763
|
-
];
|
|
3764
|
-
|
|
3765
|
-
// Add nodes
|
|
3766
|
-
for (const entity of entities) {
|
|
3767
|
-
const tier = entity.memory_tier || "reference";
|
|
3768
|
-
const color = tierColors[tier] || "#9E9E9E";
|
|
3769
|
-
const shape = typeShapes[entity.type] || "box";
|
|
3770
|
-
const label =
|
|
3771
|
-
entity.title.length > 40
|
|
3772
|
-
? entity.title.slice(0, 37) + "..."
|
|
3773
|
-
: entity.title;
|
|
3774
|
-
const safeLabel = label.replace(/"/g, '\\"');
|
|
3775
|
-
lines.push(
|
|
3776
|
-
` "${entity.id.slice(0, 8)}" [label="${safeLabel}\\n(${entity.type}, ${tier})", fillcolor="${color}", shape=${shape}];`,
|
|
3777
|
-
);
|
|
3778
|
-
}
|
|
3779
|
-
|
|
3780
|
-
// Fetch relations for each entity and add edges
|
|
3781
|
-
const entityIds = new Set(entities.map((e) => e.id));
|
|
3782
|
-
const addedEdges = new Set<string>();
|
|
3783
|
-
|
|
3784
|
-
for (const entity of entities.slice(0, 20)) {
|
|
3785
|
-
// Limit graph API calls
|
|
3786
|
-
try {
|
|
3787
|
-
const related = await client.getRelatedEntities(entity.id);
|
|
3788
|
-
|
|
3789
|
-
// Outgoing relations: { id, relation_type, target: { id, ... } }
|
|
3790
|
-
for (const raw of related.outgoing || []) {
|
|
3791
|
-
const rel = raw as {
|
|
3792
|
-
id: string;
|
|
3793
|
-
relation_type: string;
|
|
3794
|
-
source_id?: string;
|
|
3795
|
-
target_id?: string;
|
|
3796
|
-
target?: { id: string };
|
|
3797
|
-
source?: { id: string };
|
|
3798
|
-
};
|
|
3799
|
-
const sourceId = rel.source_id || entity.id;
|
|
3800
|
-
const targetId = rel.target_id || rel.target?.id;
|
|
3801
|
-
if (
|
|
3802
|
-
targetId &&
|
|
3803
|
-
entityIds.has(sourceId) &&
|
|
3804
|
-
entityIds.has(targetId)
|
|
3805
|
-
) {
|
|
3806
|
-
const edgeKey = `${sourceId}-${targetId}-${rel.relation_type}`;
|
|
3807
|
-
if (!addedEdges.has(edgeKey)) {
|
|
3808
|
-
addedEdges.add(edgeKey);
|
|
3809
|
-
lines.push(
|
|
3810
|
-
` "${sourceId.slice(0, 8)}" -> "${targetId.slice(0, 8)}" [label="${rel.relation_type}"];`,
|
|
3811
|
-
);
|
|
3812
|
-
}
|
|
3813
|
-
}
|
|
3814
|
-
}
|
|
3815
|
-
|
|
3816
|
-
// Incoming relations: { id, relation_type, source: { id, ... } }
|
|
3817
|
-
for (const raw of related.incoming || []) {
|
|
3818
|
-
const rel = raw as {
|
|
3819
|
-
id: string;
|
|
3820
|
-
relation_type: string;
|
|
3821
|
-
source_id?: string;
|
|
3822
|
-
target_id?: string;
|
|
3823
|
-
target?: { id: string };
|
|
3824
|
-
source?: { id: string };
|
|
3825
|
-
};
|
|
3826
|
-
const sourceId = rel.source_id || rel.source?.id;
|
|
3827
|
-
const targetId = rel.target_id || entity.id;
|
|
3828
|
-
if (
|
|
3829
|
-
sourceId &&
|
|
3830
|
-
entityIds.has(sourceId) &&
|
|
3831
|
-
entityIds.has(targetId)
|
|
3832
|
-
) {
|
|
3833
|
-
const edgeKey = `${sourceId}-${targetId}-${rel.relation_type}`;
|
|
3834
|
-
if (!addedEdges.has(edgeKey)) {
|
|
3835
|
-
addedEdges.add(edgeKey);
|
|
3836
|
-
lines.push(
|
|
3837
|
-
` "${sourceId.slice(0, 8)}" -> "${targetId.slice(0, 8)}" [label="${rel.relation_type}"];`,
|
|
3838
|
-
);
|
|
3839
|
-
}
|
|
3840
|
-
}
|
|
3841
|
-
}
|
|
3842
|
-
} catch {
|
|
3843
|
-
// Relation fetch failed, continue
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
3846
|
-
|
|
3847
|
-
lines.push("}");
|
|
3848
|
-
const dotGraph = lines.join("\n");
|
|
3849
|
-
|
|
3850
|
-
return {
|
|
3851
|
-
success: true,
|
|
3852
|
-
format: "dot",
|
|
3853
|
-
entityCount: entities.length,
|
|
3854
|
-
edgeCount: addedEdges.size,
|
|
3855
|
-
graph: dotGraph,
|
|
3856
|
-
message: `Exported ${entities.length} entities and ${addedEdges.size} relations as DOT graph. Use Graphviz to render: echo '<graph>' | dot -Tpng -o graph.png`,
|
|
3857
|
-
};
|
|
3858
|
-
}
|
|
3859
|
-
|
|
3860
|
-
case "harmony_prune_draft": {
|
|
3861
|
-
const workspaceId =
|
|
3862
|
-
(args.workspaceId as string) || deps.getActiveWorkspaceId();
|
|
3863
|
-
if (!workspaceId) {
|
|
3864
|
-
throw new Error(
|
|
3865
|
-
"No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
|
|
3866
|
-
);
|
|
3867
|
-
}
|
|
3868
|
-
const projectId =
|
|
3869
|
-
(args.projectId as string) || deps.getActiveProjectId() || undefined;
|
|
3870
|
-
const dryRun = args.dryRun !== false; // default true
|
|
3871
|
-
const maxAgeDays = (args.maxAgeDays as number) || 30;
|
|
3872
|
-
|
|
3873
|
-
// Fetch draft entities
|
|
3874
|
-
const result = await client.listMemoryEntities({
|
|
3875
|
-
workspace_id: workspaceId,
|
|
3876
|
-
project_id: projectId,
|
|
3877
|
-
limit: 100,
|
|
3878
|
-
});
|
|
3879
|
-
|
|
3880
|
-
const drafts = (result.entities || []).filter((e: unknown) => {
|
|
3881
|
-
const entity = e as { memory_tier?: string };
|
|
3882
|
-
return entity.memory_tier === "draft";
|
|
3883
|
-
});
|
|
3884
|
-
|
|
3885
|
-
const now = Date.now();
|
|
3886
|
-
const stale: Array<{
|
|
3887
|
-
id: string;
|
|
3888
|
-
title: string;
|
|
3889
|
-
ageDays: number;
|
|
3890
|
-
decayScore: number;
|
|
3891
|
-
}> = [];
|
|
3892
|
-
|
|
3893
|
-
for (const raw of drafts) {
|
|
3894
|
-
const entity = raw as {
|
|
3895
|
-
id: string;
|
|
3896
|
-
title: string;
|
|
3897
|
-
memory_tier: "draft" | "episode" | "reference";
|
|
3898
|
-
confidence: number;
|
|
3899
|
-
access_count: number;
|
|
3900
|
-
last_accessed_at: string | null;
|
|
3901
|
-
created_at: string;
|
|
3902
|
-
updated_at: string;
|
|
3903
|
-
};
|
|
3904
|
-
|
|
3905
|
-
const ageDays =
|
|
3906
|
-
(now - new Date(entity.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
3907
|
-
if (ageDays < maxAgeDays) continue;
|
|
3908
|
-
|
|
3909
|
-
const lifecycle = evaluateLifecycle(entity);
|
|
3910
|
-
stale.push({
|
|
3911
|
-
id: entity.id,
|
|
3912
|
-
title: entity.title,
|
|
3913
|
-
ageDays: Math.round(ageDays),
|
|
3914
|
-
decayScore: Math.round(lifecycle.decay.score * 100) / 100,
|
|
3915
|
-
});
|
|
3916
|
-
}
|
|
3917
|
-
|
|
3918
|
-
if (!dryRun) {
|
|
3919
|
-
for (const item of stale) {
|
|
3920
|
-
try {
|
|
3921
|
-
await client.deleteMemoryEntity(item.id);
|
|
3922
|
-
} catch {
|
|
3923
|
-
// Non-fatal
|
|
3924
|
-
}
|
|
3925
|
-
}
|
|
3926
|
-
}
|
|
3927
|
-
|
|
3928
|
-
return {
|
|
3929
|
-
success: true,
|
|
3930
|
-
dryRun,
|
|
3931
|
-
totalDrafts: drafts.length,
|
|
3932
|
-
staleDrafts: stale.length,
|
|
3933
|
-
pruned: dryRun ? 0 : stale.length,
|
|
3934
|
-
items: stale,
|
|
3935
|
-
message: dryRun
|
|
3936
|
-
? `Found ${stale.length} stale drafts (>${maxAgeDays} days old). Run with dryRun=false to delete.`
|
|
3937
|
-
: `Pruned ${stale.length} stale draft memories.`,
|
|
3938
|
-
};
|
|
3939
|
-
}
|
|
3940
|
-
|
|
3941
|
-
case "harmony_consolidate_memories": {
|
|
3942
|
-
const workspaceId =
|
|
3943
|
-
(args.workspaceId as string) || deps.getActiveWorkspaceId();
|
|
3944
|
-
if (!workspaceId) {
|
|
3945
|
-
throw new Error(
|
|
3946
|
-
"No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
|
|
3947
|
-
);
|
|
3948
|
-
}
|
|
3949
|
-
const projectId =
|
|
3950
|
-
(args.projectId as string) || deps.getActiveProjectId() || undefined;
|
|
3951
|
-
const dryRun = args.dryRun !== false; // default true
|
|
3952
|
-
const minClusterSize = (args.minClusterSize as number) || 2;
|
|
3953
|
-
|
|
3954
|
-
const consolidationResult = await consolidateMemories(
|
|
3955
|
-
client,
|
|
3956
|
-
workspaceId,
|
|
3957
|
-
projectId,
|
|
3958
|
-
{ dryRun, minClusterSize },
|
|
3959
|
-
);
|
|
3960
|
-
|
|
3961
|
-
return {
|
|
3962
|
-
success: true,
|
|
3963
|
-
dryRun,
|
|
3964
|
-
...consolidationResult,
|
|
3965
|
-
message: dryRun
|
|
3966
|
-
? `Found ${consolidationResult.clustersFound} clusters across ${consolidationResult.entitiesProcessed} entities. Run with dryRun=false to consolidate.`
|
|
3967
|
-
: `Consolidated ${consolidationResult.consolidated} clusters from ${consolidationResult.entitiesProcessed} entities.`,
|
|
3968
|
-
};
|
|
3969
|
-
}
|
|
3970
|
-
|
|
3971
|
-
case "harmony_get_context_manifest": {
|
|
3972
|
-
const assemblyId = z.string().min(1).max(100).parse(args.assemblyId);
|
|
3973
|
-
const manifest = getCachedManifest(assemblyId);
|
|
3974
|
-
if (!manifest) {
|
|
3975
|
-
throw new Error(
|
|
3976
|
-
`Manifest not found for assembly '${assemblyId}'. Manifests are cached in-memory and expire after server restart.`,
|
|
3977
|
-
);
|
|
3978
|
-
}
|
|
3979
|
-
return {
|
|
3980
|
-
success: true,
|
|
3981
|
-
manifest,
|
|
3982
|
-
summary: {
|
|
3983
|
-
totalIncluded: manifest.included.length,
|
|
3984
|
-
totalExcluded: manifest.excluded.length,
|
|
3985
|
-
budgetUsed: `${manifest.budgetUsed}/${manifest.budgetTotal} tokens`,
|
|
3986
|
-
tierBreakdown: manifest.tierBreakdown,
|
|
3987
|
-
procedureBreakdown: manifest.procedureBreakdown,
|
|
3988
|
-
},
|
|
3989
|
-
};
|
|
3990
|
-
}
|
|
3991
|
-
|
|
3992
|
-
case "harmony_promote_memory": {
|
|
3993
|
-
const entityId = z.string().uuid().parse(args.entityId);
|
|
3994
|
-
const targetTier = z
|
|
3995
|
-
.enum(["episode", "reference"])
|
|
3996
|
-
.parse(args.targetTier);
|
|
3997
|
-
const reason = z.string().min(1).max(500).parse(args.reason);
|
|
3998
|
-
|
|
3999
|
-
// Fetch current entity to validate promotion path
|
|
4000
|
-
const entityResult = await client.getMemoryEntity(entityId);
|
|
4001
|
-
const entity = entityResult.entity as {
|
|
4002
|
-
id: string;
|
|
4003
|
-
memory_tier?: string;
|
|
4004
|
-
title: string;
|
|
4005
|
-
};
|
|
4006
|
-
const currentTier = entity.memory_tier || "reference";
|
|
4007
|
-
|
|
4008
|
-
// Validate promotion path: draft→episode→reference
|
|
4009
|
-
const tierOrder = { draft: 0, episode: 1, reference: 2 };
|
|
4010
|
-
if (
|
|
4011
|
-
(tierOrder[targetTier] || 0) <=
|
|
4012
|
-
(tierOrder[currentTier as keyof typeof tierOrder] || 0)
|
|
4013
|
-
) {
|
|
4014
|
-
throw new Error(
|
|
4015
|
-
`Cannot promote from '${currentTier}' to '${targetTier}'. Must promote to a higher tier.`,
|
|
4016
|
-
);
|
|
4017
|
-
}
|
|
4018
|
-
|
|
4019
|
-
const result = await client.updateMemoryEntity(entityId, {
|
|
4020
|
-
memory_tier: targetTier,
|
|
4021
|
-
metadata: {
|
|
4022
|
-
promoted_from_tier: currentTier,
|
|
4023
|
-
promotion_reason: reason,
|
|
4024
|
-
promoted_at: new Date().toISOString(),
|
|
4025
|
-
},
|
|
4026
|
-
});
|
|
4027
|
-
|
|
4028
|
-
return {
|
|
4029
|
-
success: true,
|
|
4030
|
-
promoted: {
|
|
4031
|
-
from: currentTier,
|
|
4032
|
-
to: targetTier,
|
|
4033
|
-
reason,
|
|
4034
|
-
},
|
|
4035
|
-
...result,
|
|
4036
|
-
};
|
|
4037
|
-
}
|
|
4038
|
-
|
|
4039
3130
|
// ============ ONBOARDING TOOLS ============
|
|
4040
3131
|
case "harmony_signup": {
|
|
4041
3132
|
const email = z.string().email().max(254).parse(args.email);
|
|
@@ -4136,213 +3227,6 @@ async function handleToolCall(
|
|
|
4136
3227
|
};
|
|
4137
3228
|
}
|
|
4138
3229
|
|
|
4139
|
-
case "harmony_backfill_embeddings": {
|
|
4140
|
-
const workspaceId =
|
|
4141
|
-
(args.workspaceId as string) || deps.getActiveWorkspaceId();
|
|
4142
|
-
if (!workspaceId) {
|
|
4143
|
-
throw new Error(
|
|
4144
|
-
"No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
|
|
4145
|
-
);
|
|
4146
|
-
}
|
|
4147
|
-
const result = await client.backfillEmbeddings(
|
|
4148
|
-
workspaceId,
|
|
4149
|
-
args.batchSize as number | undefined,
|
|
4150
|
-
);
|
|
4151
|
-
return {
|
|
4152
|
-
success: true,
|
|
4153
|
-
...result,
|
|
4154
|
-
message:
|
|
4155
|
-
result.remaining > 0
|
|
4156
|
-
? `Processed ${result.processed} entities. ${result.remaining} still need embeddings — run again to continue.`
|
|
4157
|
-
: `All embeddings up to date. Processed ${result.processed} entities.`,
|
|
4158
|
-
};
|
|
4159
|
-
}
|
|
4160
|
-
|
|
4161
|
-
case "harmony_backfill_relations": {
|
|
4162
|
-
const workspaceId =
|
|
4163
|
-
(args.workspaceId as string) || deps.getActiveWorkspaceId();
|
|
4164
|
-
if (!workspaceId) {
|
|
4165
|
-
throw new Error(
|
|
4166
|
-
"No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
|
|
4167
|
-
);
|
|
4168
|
-
}
|
|
4169
|
-
const projectId =
|
|
4170
|
-
(args.projectId as string) || deps.getActiveProjectId() || undefined;
|
|
4171
|
-
const maxRelationsPerEntity = (args.maxRelationsPerEntity as number) || 3;
|
|
4172
|
-
|
|
4173
|
-
// Fetch all entities
|
|
4174
|
-
const { entities: allEntities } = await client.listMemoryEntities({
|
|
4175
|
-
workspace_id: workspaceId,
|
|
4176
|
-
project_id: projectId,
|
|
4177
|
-
limit: 200,
|
|
4178
|
-
});
|
|
4179
|
-
|
|
4180
|
-
let entitiesProcessed = 0;
|
|
4181
|
-
let relationsCreated = 0;
|
|
4182
|
-
|
|
4183
|
-
for (const raw of allEntities as Array<{
|
|
4184
|
-
id: string;
|
|
4185
|
-
title: string;
|
|
4186
|
-
content: string;
|
|
4187
|
-
tags?: string[];
|
|
4188
|
-
}>) {
|
|
4189
|
-
try {
|
|
4190
|
-
const result = await autoExpandGraph(
|
|
4191
|
-
client,
|
|
4192
|
-
raw.id,
|
|
4193
|
-
raw.title,
|
|
4194
|
-
raw.content || "",
|
|
4195
|
-
raw.tags || [],
|
|
4196
|
-
workspaceId,
|
|
4197
|
-
projectId,
|
|
4198
|
-
maxRelationsPerEntity,
|
|
4199
|
-
);
|
|
4200
|
-
entitiesProcessed++;
|
|
4201
|
-
relationsCreated += result.relationsCreated;
|
|
4202
|
-
} catch {
|
|
4203
|
-
// Non-fatal: continue with remaining entities
|
|
4204
|
-
}
|
|
4205
|
-
}
|
|
4206
|
-
|
|
4207
|
-
return {
|
|
4208
|
-
success: true,
|
|
4209
|
-
entitiesProcessed,
|
|
4210
|
-
relationsCreated,
|
|
4211
|
-
message: `Processed ${entitiesProcessed} entities, created ${relationsCreated} new relations.`,
|
|
4212
|
-
};
|
|
4213
|
-
}
|
|
4214
|
-
|
|
4215
|
-
case "harmony_audit_memories": {
|
|
4216
|
-
const workspaceId =
|
|
4217
|
-
(args.workspaceId as string) || deps.getActiveWorkspaceId();
|
|
4218
|
-
if (!workspaceId) {
|
|
4219
|
-
throw new Error(
|
|
4220
|
-
"No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
|
|
4221
|
-
);
|
|
4222
|
-
}
|
|
4223
|
-
const projectId =
|
|
4224
|
-
(args.projectId as string) || deps.getActiveProjectId() || undefined;
|
|
4225
|
-
|
|
4226
|
-
const report = await runMemoryAudit(client, workspaceId, projectId, {
|
|
4227
|
-
dryRun: args.dryRun as boolean | undefined,
|
|
4228
|
-
archiveBelow: args.archiveBelow as number | undefined,
|
|
4229
|
-
deleteBelow: args.deleteBelow as number | undefined,
|
|
4230
|
-
limit: args.limit as number | undefined,
|
|
4231
|
-
staleDraftAgeDays: args.staleDraftAgeDays as number | undefined,
|
|
4232
|
-
});
|
|
4233
|
-
|
|
4234
|
-
return {
|
|
4235
|
-
success: report.success,
|
|
4236
|
-
dryRun: report.dryRun,
|
|
4237
|
-
summary: report.summary,
|
|
4238
|
-
distribution: report.distribution,
|
|
4239
|
-
legacyBreakdown: report.legacyBreakdown,
|
|
4240
|
-
actionsTaken: report.actionsTaken,
|
|
4241
|
-
lowest: report.lowest,
|
|
4242
|
-
staleDrafts: report.staleDrafts,
|
|
4243
|
-
errors: report.errors,
|
|
4244
|
-
healthReport: report.healthReport,
|
|
4245
|
-
};
|
|
4246
|
-
}
|
|
4247
|
-
|
|
4248
|
-
case "harmony_cleanup_memories": {
|
|
4249
|
-
const workspaceId =
|
|
4250
|
-
(args.workspaceId as string) || deps.getActiveWorkspaceId();
|
|
4251
|
-
if (!workspaceId) {
|
|
4252
|
-
throw new Error(
|
|
4253
|
-
"No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
|
|
4254
|
-
);
|
|
4255
|
-
}
|
|
4256
|
-
const projectId =
|
|
4257
|
-
(args.projectId as string) || deps.getActiveProjectId() || undefined;
|
|
4258
|
-
|
|
4259
|
-
const validSteps = [
|
|
4260
|
-
"prune",
|
|
4261
|
-
"consolidate",
|
|
4262
|
-
"orphans",
|
|
4263
|
-
"duplicates",
|
|
4264
|
-
"backfill",
|
|
4265
|
-
"audit",
|
|
4266
|
-
];
|
|
4267
|
-
const rawSteps = args.steps as string[] | undefined;
|
|
4268
|
-
const steps = rawSteps?.filter((s) => validSteps.includes(s));
|
|
4269
|
-
if (rawSteps && steps && steps.length < rawSteps.length) {
|
|
4270
|
-
const invalid = rawSteps.filter((s) => !validSteps.includes(s));
|
|
4271
|
-
// Will appear in report.errors via the healthReport
|
|
4272
|
-
console.warn(`Unknown cleanup steps ignored: ${invalid.join(", ")}`);
|
|
4273
|
-
}
|
|
4274
|
-
|
|
4275
|
-
const report = await runMemoryCleanup(client, workspaceId, projectId, {
|
|
4276
|
-
dryRun: args.dryRun as boolean | undefined,
|
|
4277
|
-
steps: steps as
|
|
4278
|
-
| (
|
|
4279
|
-
| "prune"
|
|
4280
|
-
| "consolidate"
|
|
4281
|
-
| "orphans"
|
|
4282
|
-
| "duplicates"
|
|
4283
|
-
| "backfill"
|
|
4284
|
-
| "audit"
|
|
4285
|
-
)[]
|
|
4286
|
-
| undefined,
|
|
4287
|
-
maxAgeDays: args.maxAgeDays as number | undefined,
|
|
4288
|
-
minClusterSize: args.minClusterSize as number | undefined,
|
|
4289
|
-
orphanAgeDays: args.orphanAgeDays as number | undefined,
|
|
4290
|
-
auditArchiveBelow: args.auditArchiveBelow as number | undefined,
|
|
4291
|
-
auditDeleteBelow: args.auditDeleteBelow as number | undefined,
|
|
4292
|
-
});
|
|
4293
|
-
|
|
4294
|
-
return {
|
|
4295
|
-
success: report.success,
|
|
4296
|
-
dryRun: report.dryRun,
|
|
4297
|
-
summary: report.summary,
|
|
4298
|
-
errors: report.errors,
|
|
4299
|
-
healthReport: report.healthReport,
|
|
4300
|
-
};
|
|
4301
|
-
}
|
|
4302
|
-
|
|
4303
|
-
case "harmony_purge_memories": {
|
|
4304
|
-
const workspaceId =
|
|
4305
|
-
(args.workspaceId as string) || deps.getActiveWorkspaceId();
|
|
4306
|
-
if (!workspaceId) {
|
|
4307
|
-
throw new Error(
|
|
4308
|
-
"No workspace specified. Use harmony_set_workspace_context or provide workspaceId.",
|
|
4309
|
-
);
|
|
4310
|
-
}
|
|
4311
|
-
const projectId = (args.projectId as string) || deps.getActiveProjectId();
|
|
4312
|
-
if (!projectId) {
|
|
4313
|
-
throw new Error(
|
|
4314
|
-
"No project specified. Purge requires a project scope. Use harmony_set_project_context or provide projectId.",
|
|
4315
|
-
);
|
|
4316
|
-
}
|
|
4317
|
-
|
|
4318
|
-
const filters: PurgeFilters = {};
|
|
4319
|
-
if (args.tier) filters.tier = args.tier as PurgeFilters["tier"];
|
|
4320
|
-
if (args.scope) filters.scope = args.scope as string;
|
|
4321
|
-
if (args.type) filters.type = args.type as string;
|
|
4322
|
-
if (args.olderThanDays !== undefined)
|
|
4323
|
-
filters.olderThanDays = args.olderThanDays as number;
|
|
4324
|
-
if (args.maxConfidence !== undefined)
|
|
4325
|
-
filters.maxConfidence = args.maxConfidence as number;
|
|
4326
|
-
if (args.tags) filters.tags = args.tags as string[];
|
|
4327
|
-
|
|
4328
|
-
const report = await purgeMemories(client, workspaceId, projectId, {
|
|
4329
|
-
dryRun: args.dryRun as boolean | undefined,
|
|
4330
|
-
filters,
|
|
4331
|
-
});
|
|
4332
|
-
|
|
4333
|
-
return {
|
|
4334
|
-
success: report.success,
|
|
4335
|
-
dryRun: report.dryRun,
|
|
4336
|
-
matched: report.matched,
|
|
4337
|
-
purged: report.purged,
|
|
4338
|
-
items: report.items,
|
|
4339
|
-
errors: report.errors,
|
|
4340
|
-
message: report.dryRun
|
|
4341
|
-
? `Found ${report.matched} entities matching filters. Run with dryRun=false to delete.`
|
|
4342
|
-
: `Purged ${report.purged} of ${report.matched} matching entities.`,
|
|
4343
|
-
};
|
|
4344
|
-
}
|
|
4345
|
-
|
|
4346
3230
|
default:
|
|
4347
3231
|
throw new Error(`Unknown tool: ${name}`);
|
|
4348
3232
|
}
|