@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/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 { runLifecycleMaintenance } from "./lifecycle-maintenance.js";
61
- import { runMemoryAudit } from "./memory-audit.js";
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
- type PurgeFilters,
64
- purgeMemories,
65
- runMemoryCleanup,
66
- } from "./memory-cleanup.js";
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: "Filter by scope",
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
- // Memory tier promotion
1646
- harmony_promote_memory: {
1647
- description:
1648
- "Promote a memory entity to a higher tier: draft→episode or episode→reference. Tracks promotion reason and source.",
1649
- inputSchema: {
1650
- type: "object",
1651
- properties: {
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
- // Vector embeddings
1672
- harmony_backfill_embeddings: {
1673
- description:
1674
- "Generate vector embeddings for knowledge entities that are missing them. Run this after importing memories or when semantic search isn't finding expected results. Idempotent — only processes entities without embeddings.",
1675
- inputSchema: {
1676
- type: "object",
1677
- properties: {
1678
- workspaceId: {
1679
- type: "string",
1680
- description: "Workspace ID (optional if context set)",
1681
- },
1682
- batchSize: {
1683
- type: "number",
1684
- description: "Number of entities to process per batch (default: 50)",
1685
- },
1686
- },
1687
- required: [],
1688
- },
1689
- },
1690
- harmony_backfill_relations: {
1691
- description:
1692
- "Retroactively create semantic relations across all existing memory entities. Iterates entities and runs graph expansion on each. Run after harmony_backfill_embeddings to connect a disconnected knowledge graph.",
1693
- inputSchema: {
1694
- type: "object",
1695
- properties: {
1696
- workspaceId: {
1697
- type: "string",
1698
- description: "Workspace ID (optional if context set)",
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 with defaults
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
- initMemorySession(cardId, agentIdentifier, agentName);
2757
-
2758
- // Prefetch relevant context (non-blocking, best-effort)
2759
- let prefetchedMemoryIds: string[] = [];
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: prefetchedMemoryIds.length,
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
- // Mid-session learning extraction (fire-and-forget)
2879
- let midSessionLearnings = 0;
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
- // Parse MCP-specific context options
3051
- const contextOptions: Record<string, boolean> = {};
3052
- if (args.includeSubtasks !== undefined) {
3053
- contextOptions.includeSubtasks =
3054
- args.includeSubtasks === true || args.includeSubtasks === "true";
3055
- }
3056
- if (args.includeLinks !== undefined) {
3057
- contextOptions.includeLinks =
3058
- args.includeLinks === true || args.includeLinks === "true";
3059
- }
3060
- if (args.includeDescription !== undefined) {
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
- // Delegate to the shared prompt generation pipeline
3067
- const result = await client.generateCardPrompt({
3068
- cardId,
3069
- workspaceId: deps.getActiveWorkspaceId() || "",
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
- ...result,
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
- // Use session's agent identifier if available, otherwise null
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: (args.scope as string) || "project",
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
- // Semantic contradiction detection (uses hybrid search instead of naive type+tag matching)
3136
- let potentialContradictions: Array<{
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 queryOpts = {
3191
- workspace_id: workspaceId,
3192
- project_id:
3193
- (args.projectId as string) || deps.getActiveProjectId() || undefined,
3194
- type: args.type as string | undefined,
3195
- scope: args.scope as string | undefined,
3196
- tags: args.tags as string[] | undefined,
3197
- min_confidence: args.minConfidence as number | undefined,
3198
- q: args.query as string | undefined,
3199
- limit: args.limit as number | undefined,
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
- // Fetch both markdown (for display) and JSON (for lifecycle evaluation) in parallel
3203
- const [markdown, { entities }] = await Promise.all([
3204
- client.listMemoryEntitiesMarkdown(queryOpts),
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
- // Evaluate lifecycle and auto-promote eligible entities (fire-and-forget)
3209
- if (entities.length > 0) {
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
- entities.map(async (entity: any) => {
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: don't fail recall if promotion check fails
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
- return markdown || "No memories found.";
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
- detectContradictions(
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
  }