@gethmy/mcp 2.16.0 → 2.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1499,6 +1499,21 @@ var TIMINGS = {
1499
1499
  QUERY_STALE_TIME: 1000 * 60 * 5,
1500
1500
  QUERY_GC_TIME: 1000 * 60 * 60 * 24
1501
1501
  };
1502
+ // ../harmony-shared/dist/playbookStage.js
1503
+ var STAGE_DAEMON_OWNED_TOOLS = [
1504
+ "mcp__harmony__harmony_end_agent_session",
1505
+ "mcp__harmony__harmony_start_agent_session",
1506
+ "mcp__harmony__harmony_move_card"
1507
+ ];
1508
+ // ../harmony-shared/dist/reviewTools.js
1509
+ var REVIEW_DISALLOWED_TOOLS = [
1510
+ ...STAGE_DAEMON_OWNED_TOOLS,
1511
+ "mcp__harmony__harmony_update_card",
1512
+ "mcp__harmony__harmony_create_subtask",
1513
+ "mcp__harmony__harmony_update_subtask",
1514
+ "mcp__harmony__harmony_delete_subtask",
1515
+ "mcp__harmony__harmony_toggle_subtask"
1516
+ ];
1502
1517
  // ../harmony-shared/dist/stageHandoff.js
1503
1518
  var HANDOFF_MARKER = "harmony:stage-handoff";
1504
1519
  var HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
@@ -1761,6 +1776,9 @@ class HarmonyApiClient {
1761
1776
  async getWorkspaceMembers(workspaceId) {
1762
1777
  return this.request("GET", `/workspaces/${workspaceId}/members`);
1763
1778
  }
1779
+ async getAuthContext() {
1780
+ return this.request("GET", "/auth/context");
1781
+ }
1764
1782
  async listWorkspaceAgents(workspaceId) {
1765
1783
  return this.request("GET", `/workspaces/${workspaceId}/agents`);
1766
1784
  }
@@ -2032,6 +2050,8 @@ class HarmonyApiClient {
2032
2050
  params.set("offset", String(options.offset));
2033
2051
  if (options.include_superseded)
2034
2052
  params.set("include_superseded", "true");
2053
+ if (options.include_episodes)
2054
+ params.set("include_episodes", "true");
2035
2055
  return this.request("GET", `/memory/entities?${params.toString()}`);
2036
2056
  }
2037
2057
  async getMemoryEntity(entityId) {
@@ -2047,7 +2067,8 @@ class HarmonyApiClient {
2047
2067
  const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
2048
2068
  project_id: options.projectId,
2049
2069
  type: options.type?.length === 1 ? options.type[0] : undefined,
2050
- limit: fetchLimit
2070
+ limit: fetchLimit,
2071
+ include_episodes: options.includeEpisodes
2051
2072
  });
2052
2073
  entities = search.entities ?? [];
2053
2074
  } else {
@@ -2058,7 +2079,8 @@ class HarmonyApiClient {
2058
2079
  type: options.type?.length === 1 ? options.type[0] : undefined,
2059
2080
  tags: options.tags,
2060
2081
  min_confidence: options.minConfidence,
2061
- limit: fetchLimit
2082
+ limit: fetchLimit,
2083
+ include_episodes: options.includeEpisodes
2062
2084
  });
2063
2085
  entities = list.entities ?? [];
2064
2086
  }
@@ -2119,6 +2141,8 @@ class HarmonyApiClient {
2119
2141
  params.append("tags", tag);
2120
2142
  if (options?.include_superseded)
2121
2143
  params.set("include_superseded", "true");
2144
+ if (options?.include_episodes)
2145
+ params.set("include_episodes", "true");
2122
2146
  return this.request("GET", `/memory/search?${params.toString()}`);
2123
2147
  }
2124
2148
  async getVaultIndex(options) {
@@ -2130,6 +2154,8 @@ class HarmonyApiClient {
2130
2154
  params.set("type", options.type);
2131
2155
  if (options.limit !== undefined)
2132
2156
  params.set("limit", String(options.limit));
2157
+ if (options.include_episodes)
2158
+ params.set("include_episodes", "true");
2133
2159
  return this.request("GET", `/memory/index?${params.toString()}`);
2134
2160
  }
2135
2161
  async getVaultIndexMarkdown(options) {
@@ -2141,6 +2167,8 @@ class HarmonyApiClient {
2141
2167
  params.set("type", options.type);
2142
2168
  if (options.limit !== undefined)
2143
2169
  params.set("limit", String(options.limit));
2170
+ if (options.include_episodes)
2171
+ params.set("include_episodes", "true");
2144
2172
  return this.requestRaw("GET", `/memory/index?${params.toString()}`, undefined, {
2145
2173
  accept: "text/markdown"
2146
2174
  });
@@ -2198,6 +2226,8 @@ class HarmonyApiClient {
2198
2226
  params.set("type", options.type);
2199
2227
  if (options?.limit !== undefined)
2200
2228
  params.set("limit", String(options.limit));
2229
+ if (options?.include_episodes)
2230
+ params.set("include_episodes", "true");
2201
2231
  return this.requestRaw("GET", `/memory/search?${params.toString()}`, undefined, {
2202
2232
  accept: "text/markdown"
2203
2233
  });
@@ -2469,6 +2499,7 @@ var AUTO_START_TRIGGERS = new Set([
2469
2499
  ]);
2470
2500
  var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
2471
2501
  var CHECK_INTERVAL_MS = 60 * 1000;
2502
+ var HEARTBEAT_ACTIVITY_WINDOW_MS = 25 * 60 * 1000;
2472
2503
  var DEFAULT_SCOPE = "__default__";
2473
2504
  var scopes = new Map;
2474
2505
  var inactivityTimer = null;
@@ -2606,17 +2637,25 @@ function noteSessionStatus(cardId, status, scopeId = DEFAULT_SCOPE) {
2606
2637
  session.status = status;
2607
2638
  }
2608
2639
  function heartbeatActiveSessions() {
2640
+ const now = Date.now();
2609
2641
  for (const scope of scopes.values()) {
2610
2642
  const client3 = scope.clientGetter?.();
2611
2643
  if (!client3)
2612
2644
  continue;
2613
- for (const session of scope.sessions.values()) {
2645
+ for (const [cardId, session] of [...scope.sessions.entries()]) {
2614
2646
  if ((session.status ?? "working") !== "working")
2615
2647
  continue;
2648
+ if (now - session.lastActivityAt > HEARTBEAT_ACTIVITY_WINDOW_MS)
2649
+ continue;
2616
2650
  client3.updateAgentProgress(session.cardId, {
2617
2651
  agentIdentifier: session.agentIdentifier,
2618
2652
  agentName: session.agentName,
2619
- status: "working"
2653
+ status: "working",
2654
+ noCreate: true
2655
+ }).then((res) => {
2656
+ if (res?.session === null && scope.sessions.get(cardId) === session) {
2657
+ scope.sessions.delete(cardId);
2658
+ }
2620
2659
  }).catch(() => {});
2621
2660
  }
2622
2661
  }
@@ -4833,6 +4872,10 @@ var TOOLS = {
4833
4872
  include_superseded: {
4834
4873
  type: "boolean",
4835
4874
  description: "Include rows whose `superseded_at` is set (default false). Use when investigating supersede chains."
4875
+ },
4876
+ include_episodes: {
4877
+ type: "boolean",
4878
+ description: "Include agent-run episodes — the daemon's per-run implement/review captures (default false). Excluded by default so recall surfaces curated, durable memory. Set true to audit the agent's run history."
4836
4879
  }
4837
4880
  }
4838
4881
  }
@@ -5018,7 +5061,11 @@ var TOOLS = {
5018
5061
  type: "string",
5019
5062
  description: "Project ID (optional)"
5020
5063
  },
5021
- limit: { type: "number", description: "Max results (default: 20)" }
5064
+ limit: { type: "number", description: "Max results (default: 20)" },
5065
+ include_episodes: {
5066
+ type: "boolean",
5067
+ description: "Include agent-run episodes — the daemon's per-run captures (default false). Excluded by default so search surfaces curated memory."
5068
+ }
5022
5069
  },
5023
5070
  required: ["query"]
5024
5071
  }
@@ -5059,6 +5106,10 @@ var TOOLS = {
5059
5106
  limit: {
5060
5107
  type: "number",
5061
5108
  description: "Max entities to return (default: 200)"
5109
+ },
5110
+ include_episodes: {
5111
+ type: "boolean",
5112
+ description: "Include agent-run episodes — the daemon's per-run captures (default false). Excluded by default so the index shows curated vault content, not run noise."
5062
5113
  }
5063
5114
  },
5064
5115
  required: []
@@ -6124,6 +6175,23 @@ async function handleToolCall(name, args, deps) {
6124
6175
  }
6125
6176
  case "harmony_set_workspace_context": {
6126
6177
  const workspaceId = z.string().uuid().parse(args.workspaceId);
6178
+ const { workspaces } = await client3.listWorkspaces();
6179
+ const available = workspaces ?? [];
6180
+ const match = available.find((w) => w?.id === workspaceId);
6181
+ if (!match) {
6182
+ const options = available.filter((w) => w?.id).map((w) => ` - ${w.name ?? "(unnamed)"} (${w.id})`).join(`
6183
+ `);
6184
+ throw new Error(`Workspace ${workspaceId} is not available to this connection.
6185
+
6186
+ ` + `This MCP connection is authorized for a specific workspace, chosen ` + `during OAuth consent — it cannot read or write any other workspace, ` + `even ones you're a member of.
6187
+
6188
+ ` + (options ? `Available here:
6189
+ ${options}
6190
+
6191
+ ` : `No workspaces are available to this connection.
6192
+
6193
+ `) + `To work in a different workspace, run /mcp to reconnect and select ` + `it on the consent screen.`);
6194
+ }
6127
6195
  deps.setActiveWorkspace(workspaceId);
6128
6196
  return { success: true, activeWorkspaceId: workspaceId };
6129
6197
  }
@@ -6486,6 +6554,7 @@ async function handleToolCall(name, args, deps) {
6486
6554
  const topK = args.top_k;
6487
6555
  const budgetTokens = args.budget_tokens;
6488
6556
  const includeSuperseded = Boolean(args.include_superseded);
6557
+ const includeEpisodes = Boolean(args.include_episodes);
6489
6558
  const queryText = args.query;
6490
6559
  const fetchLimit = Math.min(Math.max(requestedLimit ?? 20, topK ?? 0, 50), 200);
6491
6560
  const projectId = args.projectId || deps.getActiveProjectId() || undefined;
@@ -6502,7 +6571,8 @@ async function handleToolCall(name, args, deps) {
6502
6571
  type: args.type,
6503
6572
  limit: fetchLimit,
6504
6573
  tags: requestedTags && requestedTags.length > 0 ? requestedTags : undefined,
6505
- include_superseded: includeSuperseded
6574
+ include_superseded: includeSuperseded,
6575
+ include_episodes: includeEpisodes
6506
6576
  });
6507
6577
  entities = searchResult.entities ?? [];
6508
6578
  const minConfidence = args.minConfidence;
@@ -6525,7 +6595,8 @@ async function handleToolCall(name, args, deps) {
6525
6595
  tags: args.tags,
6526
6596
  min_confidence: args.minConfidence,
6527
6597
  limit: fetchLimit,
6528
- include_superseded: includeSuperseded
6598
+ include_superseded: includeSuperseded,
6599
+ include_episodes: includeEpisodes
6529
6600
  });
6530
6601
  entities = listed;
6531
6602
  if (excludeSessionFromLongTerm) {
@@ -6745,7 +6816,8 @@ async function handleToolCall(name, args, deps) {
6745
6816
  const markdown = await client3.searchMemoryEntitiesMarkdown(workspaceId, query, {
6746
6817
  project_id: args.projectId || deps.getActiveProjectId() || undefined,
6747
6818
  type: args.type,
6748
- limit: args.limit
6819
+ limit: args.limit,
6820
+ include_episodes: Boolean(args.include_episodes)
6749
6821
  });
6750
6822
  const searchMemSession = getActiveMemorySession();
6751
6823
  if (searchMemSession) {
@@ -6762,7 +6834,8 @@ async function handleToolCall(name, args, deps) {
6762
6834
  workspace_id: workspaceId,
6763
6835
  project_id: args.projectId || deps.getActiveProjectId() || undefined,
6764
6836
  type: args.type,
6765
- limit: args.limit
6837
+ limit: args.limit,
6838
+ include_episodes: Boolean(args.include_episodes)
6766
6839
  });
6767
6840
  return markdown || "No entities found.";
6768
6841
  }
package/dist/index.js CHANGED
@@ -1494,6 +1494,21 @@ var TIMINGS = {
1494
1494
  QUERY_STALE_TIME: 1000 * 60 * 5,
1495
1495
  QUERY_GC_TIME: 1000 * 60 * 60 * 24
1496
1496
  };
1497
+ // ../harmony-shared/dist/playbookStage.js
1498
+ var STAGE_DAEMON_OWNED_TOOLS = [
1499
+ "mcp__harmony__harmony_end_agent_session",
1500
+ "mcp__harmony__harmony_start_agent_session",
1501
+ "mcp__harmony__harmony_move_card"
1502
+ ];
1503
+ // ../harmony-shared/dist/reviewTools.js
1504
+ var REVIEW_DISALLOWED_TOOLS = [
1505
+ ...STAGE_DAEMON_OWNED_TOOLS,
1506
+ "mcp__harmony__harmony_update_card",
1507
+ "mcp__harmony__harmony_create_subtask",
1508
+ "mcp__harmony__harmony_update_subtask",
1509
+ "mcp__harmony__harmony_delete_subtask",
1510
+ "mcp__harmony__harmony_toggle_subtask"
1511
+ ];
1497
1512
  // ../harmony-shared/dist/stageHandoff.js
1498
1513
  var HANDOFF_MARKER = "harmony:stage-handoff";
1499
1514
  var HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
@@ -1756,6 +1771,9 @@ class HarmonyApiClient {
1756
1771
  async getWorkspaceMembers(workspaceId) {
1757
1772
  return this.request("GET", `/workspaces/${workspaceId}/members`);
1758
1773
  }
1774
+ async getAuthContext() {
1775
+ return this.request("GET", "/auth/context");
1776
+ }
1759
1777
  async listWorkspaceAgents(workspaceId) {
1760
1778
  return this.request("GET", `/workspaces/${workspaceId}/agents`);
1761
1779
  }
@@ -2027,6 +2045,8 @@ class HarmonyApiClient {
2027
2045
  params.set("offset", String(options.offset));
2028
2046
  if (options.include_superseded)
2029
2047
  params.set("include_superseded", "true");
2048
+ if (options.include_episodes)
2049
+ params.set("include_episodes", "true");
2030
2050
  return this.request("GET", `/memory/entities?${params.toString()}`);
2031
2051
  }
2032
2052
  async getMemoryEntity(entityId) {
@@ -2042,7 +2062,8 @@ class HarmonyApiClient {
2042
2062
  const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
2043
2063
  project_id: options.projectId,
2044
2064
  type: options.type?.length === 1 ? options.type[0] : undefined,
2045
- limit: fetchLimit
2065
+ limit: fetchLimit,
2066
+ include_episodes: options.includeEpisodes
2046
2067
  });
2047
2068
  entities = search.entities ?? [];
2048
2069
  } else {
@@ -2053,7 +2074,8 @@ class HarmonyApiClient {
2053
2074
  type: options.type?.length === 1 ? options.type[0] : undefined,
2054
2075
  tags: options.tags,
2055
2076
  min_confidence: options.minConfidence,
2056
- limit: fetchLimit
2077
+ limit: fetchLimit,
2078
+ include_episodes: options.includeEpisodes
2057
2079
  });
2058
2080
  entities = list.entities ?? [];
2059
2081
  }
@@ -2114,6 +2136,8 @@ class HarmonyApiClient {
2114
2136
  params.append("tags", tag);
2115
2137
  if (options?.include_superseded)
2116
2138
  params.set("include_superseded", "true");
2139
+ if (options?.include_episodes)
2140
+ params.set("include_episodes", "true");
2117
2141
  return this.request("GET", `/memory/search?${params.toString()}`);
2118
2142
  }
2119
2143
  async getVaultIndex(options) {
@@ -2125,6 +2149,8 @@ class HarmonyApiClient {
2125
2149
  params.set("type", options.type);
2126
2150
  if (options.limit !== undefined)
2127
2151
  params.set("limit", String(options.limit));
2152
+ if (options.include_episodes)
2153
+ params.set("include_episodes", "true");
2128
2154
  return this.request("GET", `/memory/index?${params.toString()}`);
2129
2155
  }
2130
2156
  async getVaultIndexMarkdown(options) {
@@ -2136,6 +2162,8 @@ class HarmonyApiClient {
2136
2162
  params.set("type", options.type);
2137
2163
  if (options.limit !== undefined)
2138
2164
  params.set("limit", String(options.limit));
2165
+ if (options.include_episodes)
2166
+ params.set("include_episodes", "true");
2139
2167
  return this.requestRaw("GET", `/memory/index?${params.toString()}`, undefined, {
2140
2168
  accept: "text/markdown"
2141
2169
  });
@@ -2193,6 +2221,8 @@ class HarmonyApiClient {
2193
2221
  params.set("type", options.type);
2194
2222
  if (options?.limit !== undefined)
2195
2223
  params.set("limit", String(options.limit));
2224
+ if (options?.include_episodes)
2225
+ params.set("include_episodes", "true");
2196
2226
  return this.requestRaw("GET", `/memory/search?${params.toString()}`, undefined, {
2197
2227
  accept: "text/markdown"
2198
2228
  });
@@ -2464,6 +2494,7 @@ var AUTO_START_TRIGGERS = new Set([
2464
2494
  ]);
2465
2495
  var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
2466
2496
  var CHECK_INTERVAL_MS = 60 * 1000;
2497
+ var HEARTBEAT_ACTIVITY_WINDOW_MS = 25 * 60 * 1000;
2467
2498
  var DEFAULT_SCOPE = "__default__";
2468
2499
  var scopes = new Map;
2469
2500
  var inactivityTimer = null;
@@ -2601,17 +2632,25 @@ function noteSessionStatus(cardId, status, scopeId = DEFAULT_SCOPE) {
2601
2632
  session.status = status;
2602
2633
  }
2603
2634
  function heartbeatActiveSessions() {
2635
+ const now = Date.now();
2604
2636
  for (const scope of scopes.values()) {
2605
2637
  const client3 = scope.clientGetter?.();
2606
2638
  if (!client3)
2607
2639
  continue;
2608
- for (const session of scope.sessions.values()) {
2640
+ for (const [cardId, session] of [...scope.sessions.entries()]) {
2609
2641
  if ((session.status ?? "working") !== "working")
2610
2642
  continue;
2643
+ if (now - session.lastActivityAt > HEARTBEAT_ACTIVITY_WINDOW_MS)
2644
+ continue;
2611
2645
  client3.updateAgentProgress(session.cardId, {
2612
2646
  agentIdentifier: session.agentIdentifier,
2613
2647
  agentName: session.agentName,
2614
- status: "working"
2648
+ status: "working",
2649
+ noCreate: true
2650
+ }).then((res) => {
2651
+ if (res?.session === null && scope.sessions.get(cardId) === session) {
2652
+ scope.sessions.delete(cardId);
2653
+ }
2615
2654
  }).catch(() => {});
2616
2655
  }
2617
2656
  }
@@ -4828,6 +4867,10 @@ var TOOLS = {
4828
4867
  include_superseded: {
4829
4868
  type: "boolean",
4830
4869
  description: "Include rows whose `superseded_at` is set (default false). Use when investigating supersede chains."
4870
+ },
4871
+ include_episodes: {
4872
+ type: "boolean",
4873
+ description: "Include agent-run episodes — the daemon's per-run implement/review captures (default false). Excluded by default so recall surfaces curated, durable memory. Set true to audit the agent's run history."
4831
4874
  }
4832
4875
  }
4833
4876
  }
@@ -5013,7 +5056,11 @@ var TOOLS = {
5013
5056
  type: "string",
5014
5057
  description: "Project ID (optional)"
5015
5058
  },
5016
- limit: { type: "number", description: "Max results (default: 20)" }
5059
+ limit: { type: "number", description: "Max results (default: 20)" },
5060
+ include_episodes: {
5061
+ type: "boolean",
5062
+ description: "Include agent-run episodes — the daemon's per-run captures (default false). Excluded by default so search surfaces curated memory."
5063
+ }
5017
5064
  },
5018
5065
  required: ["query"]
5019
5066
  }
@@ -5054,6 +5101,10 @@ var TOOLS = {
5054
5101
  limit: {
5055
5102
  type: "number",
5056
5103
  description: "Max entities to return (default: 200)"
5104
+ },
5105
+ include_episodes: {
5106
+ type: "boolean",
5107
+ description: "Include agent-run episodes — the daemon's per-run captures (default false). Excluded by default so the index shows curated vault content, not run noise."
5057
5108
  }
5058
5109
  },
5059
5110
  required: []
@@ -6119,6 +6170,23 @@ async function handleToolCall(name, args, deps) {
6119
6170
  }
6120
6171
  case "harmony_set_workspace_context": {
6121
6172
  const workspaceId = z.string().uuid().parse(args.workspaceId);
6173
+ const { workspaces } = await client3.listWorkspaces();
6174
+ const available = workspaces ?? [];
6175
+ const match = available.find((w) => w?.id === workspaceId);
6176
+ if (!match) {
6177
+ const options = available.filter((w) => w?.id).map((w) => ` - ${w.name ?? "(unnamed)"} (${w.id})`).join(`
6178
+ `);
6179
+ throw new Error(`Workspace ${workspaceId} is not available to this connection.
6180
+
6181
+ ` + `This MCP connection is authorized for a specific workspace, chosen ` + `during OAuth consent — it cannot read or write any other workspace, ` + `even ones you're a member of.
6182
+
6183
+ ` + (options ? `Available here:
6184
+ ${options}
6185
+
6186
+ ` : `No workspaces are available to this connection.
6187
+
6188
+ `) + `To work in a different workspace, run /mcp to reconnect and select ` + `it on the consent screen.`);
6189
+ }
6122
6190
  deps.setActiveWorkspace(workspaceId);
6123
6191
  return { success: true, activeWorkspaceId: workspaceId };
6124
6192
  }
@@ -6481,6 +6549,7 @@ async function handleToolCall(name, args, deps) {
6481
6549
  const topK = args.top_k;
6482
6550
  const budgetTokens = args.budget_tokens;
6483
6551
  const includeSuperseded = Boolean(args.include_superseded);
6552
+ const includeEpisodes = Boolean(args.include_episodes);
6484
6553
  const queryText = args.query;
6485
6554
  const fetchLimit = Math.min(Math.max(requestedLimit ?? 20, topK ?? 0, 50), 200);
6486
6555
  const projectId = args.projectId || deps.getActiveProjectId() || undefined;
@@ -6497,7 +6566,8 @@ async function handleToolCall(name, args, deps) {
6497
6566
  type: args.type,
6498
6567
  limit: fetchLimit,
6499
6568
  tags: requestedTags && requestedTags.length > 0 ? requestedTags : undefined,
6500
- include_superseded: includeSuperseded
6569
+ include_superseded: includeSuperseded,
6570
+ include_episodes: includeEpisodes
6501
6571
  });
6502
6572
  entities = searchResult.entities ?? [];
6503
6573
  const minConfidence = args.minConfidence;
@@ -6520,7 +6590,8 @@ async function handleToolCall(name, args, deps) {
6520
6590
  tags: args.tags,
6521
6591
  min_confidence: args.minConfidence,
6522
6592
  limit: fetchLimit,
6523
- include_superseded: includeSuperseded
6593
+ include_superseded: includeSuperseded,
6594
+ include_episodes: includeEpisodes
6524
6595
  });
6525
6596
  entities = listed;
6526
6597
  if (excludeSessionFromLongTerm) {
@@ -6740,7 +6811,8 @@ async function handleToolCall(name, args, deps) {
6740
6811
  const markdown = await client3.searchMemoryEntitiesMarkdown(workspaceId, query, {
6741
6812
  project_id: args.projectId || deps.getActiveProjectId() || undefined,
6742
6813
  type: args.type,
6743
- limit: args.limit
6814
+ limit: args.limit,
6815
+ include_episodes: Boolean(args.include_episodes)
6744
6816
  });
6745
6817
  const searchMemSession = getActiveMemorySession();
6746
6818
  if (searchMemSession) {
@@ -6757,7 +6829,8 @@ async function handleToolCall(name, args, deps) {
6757
6829
  workspace_id: workspaceId,
6758
6830
  project_id: args.projectId || deps.getActiveProjectId() || undefined,
6759
6831
  type: args.type,
6760
- limit: args.limit
6832
+ limit: args.limit,
6833
+ include_episodes: Boolean(args.include_episodes)
6761
6834
  });
6762
6835
  return markdown || "No entities found.";
6763
6836
  }
@@ -946,6 +946,21 @@ var TIMINGS = {
946
946
  QUERY_STALE_TIME: 1000 * 60 * 5,
947
947
  QUERY_GC_TIME: 1000 * 60 * 60 * 24
948
948
  };
949
+ // ../harmony-shared/dist/playbookStage.js
950
+ var STAGE_DAEMON_OWNED_TOOLS = [
951
+ "mcp__harmony__harmony_end_agent_session",
952
+ "mcp__harmony__harmony_start_agent_session",
953
+ "mcp__harmony__harmony_move_card"
954
+ ];
955
+ // ../harmony-shared/dist/reviewTools.js
956
+ var REVIEW_DISALLOWED_TOOLS = [
957
+ ...STAGE_DAEMON_OWNED_TOOLS,
958
+ "mcp__harmony__harmony_update_card",
959
+ "mcp__harmony__harmony_create_subtask",
960
+ "mcp__harmony__harmony_update_subtask",
961
+ "mcp__harmony__harmony_delete_subtask",
962
+ "mcp__harmony__harmony_toggle_subtask"
963
+ ];
949
964
  // ../harmony-shared/dist/stageHandoff.js
950
965
  var HANDOFF_MARKER = "harmony:stage-handoff";
951
966
  var HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
@@ -1208,6 +1223,9 @@ class HarmonyApiClient {
1208
1223
  async getWorkspaceMembers(workspaceId) {
1209
1224
  return this.request("GET", `/workspaces/${workspaceId}/members`);
1210
1225
  }
1226
+ async getAuthContext() {
1227
+ return this.request("GET", "/auth/context");
1228
+ }
1211
1229
  async listWorkspaceAgents(workspaceId) {
1212
1230
  return this.request("GET", `/workspaces/${workspaceId}/agents`);
1213
1231
  }
@@ -1479,6 +1497,8 @@ class HarmonyApiClient {
1479
1497
  params.set("offset", String(options.offset));
1480
1498
  if (options.include_superseded)
1481
1499
  params.set("include_superseded", "true");
1500
+ if (options.include_episodes)
1501
+ params.set("include_episodes", "true");
1482
1502
  return this.request("GET", `/memory/entities?${params.toString()}`);
1483
1503
  }
1484
1504
  async getMemoryEntity(entityId) {
@@ -1494,7 +1514,8 @@ class HarmonyApiClient {
1494
1514
  const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
1495
1515
  project_id: options.projectId,
1496
1516
  type: options.type?.length === 1 ? options.type[0] : undefined,
1497
- limit: fetchLimit
1517
+ limit: fetchLimit,
1518
+ include_episodes: options.includeEpisodes
1498
1519
  });
1499
1520
  entities = search.entities ?? [];
1500
1521
  } else {
@@ -1505,7 +1526,8 @@ class HarmonyApiClient {
1505
1526
  type: options.type?.length === 1 ? options.type[0] : undefined,
1506
1527
  tags: options.tags,
1507
1528
  min_confidence: options.minConfidence,
1508
- limit: fetchLimit
1529
+ limit: fetchLimit,
1530
+ include_episodes: options.includeEpisodes
1509
1531
  });
1510
1532
  entities = list.entities ?? [];
1511
1533
  }
@@ -1566,6 +1588,8 @@ class HarmonyApiClient {
1566
1588
  params.append("tags", tag);
1567
1589
  if (options?.include_superseded)
1568
1590
  params.set("include_superseded", "true");
1591
+ if (options?.include_episodes)
1592
+ params.set("include_episodes", "true");
1569
1593
  return this.request("GET", `/memory/search?${params.toString()}`);
1570
1594
  }
1571
1595
  async getVaultIndex(options) {
@@ -1577,6 +1601,8 @@ class HarmonyApiClient {
1577
1601
  params.set("type", options.type);
1578
1602
  if (options.limit !== undefined)
1579
1603
  params.set("limit", String(options.limit));
1604
+ if (options.include_episodes)
1605
+ params.set("include_episodes", "true");
1580
1606
  return this.request("GET", `/memory/index?${params.toString()}`);
1581
1607
  }
1582
1608
  async getVaultIndexMarkdown(options) {
@@ -1588,6 +1614,8 @@ class HarmonyApiClient {
1588
1614
  params.set("type", options.type);
1589
1615
  if (options.limit !== undefined)
1590
1616
  params.set("limit", String(options.limit));
1617
+ if (options.include_episodes)
1618
+ params.set("include_episodes", "true");
1591
1619
  return this.requestRaw("GET", `/memory/index?${params.toString()}`, undefined, {
1592
1620
  accept: "text/markdown"
1593
1621
  });
@@ -1645,6 +1673,8 @@ class HarmonyApiClient {
1645
1673
  params.set("type", options.type);
1646
1674
  if (options?.limit !== undefined)
1647
1675
  params.set("limit", String(options.limit));
1676
+ if (options?.include_episodes)
1677
+ params.set("include_episodes", "true");
1648
1678
  return this.requestRaw("GET", `/memory/search?${params.toString()}`, undefined, {
1649
1679
  accept: "text/markdown"
1650
1680
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.16.0",
3
+ "version": "2.17.1",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/api-client.ts CHANGED
@@ -461,6 +461,22 @@ export class HarmonyApiClient {
461
461
  return this.request("GET", `/workspaces/${workspaceId}/members`);
462
462
  }
463
463
 
464
+ /**
465
+ * The auth user THIS client's credential resolves to server-side — the id
466
+ * harmony-api stamps onto sessions it creates for this caller. The agent
467
+ * daemon uses it to assert its configured `userEmail` and its API key are the
468
+ * same user; a divergence would make it fail closed on its own comment
469
+ * artifacts (card #693). `GET /v1/auth/context` is long-deployed (the MCP
470
+ * transport authenticates every request through it).
471
+ */
472
+ async getAuthContext(): Promise<{
473
+ userId: string;
474
+ source: "api_key" | "oauth" | "jwt";
475
+ workspaceId: string | null;
476
+ }> {
477
+ return this.request("GET", "/auth/context");
478
+ }
479
+
464
480
  async listWorkspaceAgents(
465
481
  workspaceId: string,
466
482
  ): Promise<{ agents: WorkspaceAgent[] }> {
@@ -1067,6 +1083,12 @@ export class HarmonyApiClient {
1067
1083
  recoveryBranch?: string;
1068
1084
  attemptNumber?: number;
1069
1085
  maxAttempts?: number;
1086
+ /**
1087
+ * Update-only: never create a session if none is live, answer
1088
+ * `{session: null}` instead. Set by the auto-session heartbeat, which must
1089
+ * report on a session but must never bring one back from the dead (#696).
1090
+ */
1091
+ noCreate?: boolean;
1070
1092
  },
1071
1093
  ): Promise<{ session: unknown; created: boolean }> {
1072
1094
  return this.request("POST", `/cards/${cardId}/agent-context`, data);
@@ -1075,7 +1097,13 @@ export class HarmonyApiClient {
1075
1097
  async endAgentSession(
1076
1098
  cardId: string,
1077
1099
  data?: {
1078
- status?: "completed" | "paused" | "failed";
1100
+ /**
1101
+ * `cancelled` marks a HUMAN stop specifically — it is what arms the
1102
+ * server's human-stop cooldown (card #663). Never use it for a graceful
1103
+ * shutdown or an internal abort, which must stay `paused`/`failed` so the
1104
+ * card can be re-picked immediately (card #696).
1105
+ */
1106
+ status?: "completed" | "paused" | "failed" | "cancelled";
1079
1107
  progressPercent?: number;
1080
1108
  costCents?: number;
1081
1109
  inputTokens?: number;
@@ -1185,6 +1213,9 @@ export class HarmonyApiClient {
1185
1213
  limit?: number;
1186
1214
  offset?: number;
1187
1215
  include_superseded?: boolean;
1216
+ // Opt in to agent-run episodes (default excluded, #677). Only the daemon's
1217
+ // own rolling-episode lookup should set this.
1218
+ include_episodes?: boolean;
1188
1219
  }): Promise<{ entities: unknown[]; count: number }> {
1189
1220
  const params = new URLSearchParams();
1190
1221
  params.set("workspace_id", options.workspace_id);
@@ -1201,6 +1232,7 @@ export class HarmonyApiClient {
1201
1232
  if (options.offset !== undefined)
1202
1233
  params.set("offset", String(options.offset));
1203
1234
  if (options.include_superseded) params.set("include_superseded", "true");
1235
+ if (options.include_episodes) params.set("include_episodes", "true");
1204
1236
  return this.request("GET", `/memory/entities?${params.toString()}`);
1205
1237
  }
1206
1238
 
@@ -1249,6 +1281,9 @@ export class HarmonyApiClient {
1249
1281
  tags?: string[];
1250
1282
  minConfidence?: number;
1251
1283
  topK?: number;
1284
+ // Opt in to agent-run episodes (default excluded server-side, #677). The
1285
+ // daemon sets this when looking up its own rolling implement/review episode.
1286
+ includeEpisodes?: boolean;
1252
1287
  }): Promise<{ entities: unknown[] }> {
1253
1288
  // Over-fetch beyond topK so client-side filters (multi-type, memory_tier,
1254
1289
  // tags) have headroom — matches the MCP server's recall path (server.ts).
@@ -1265,6 +1300,7 @@ export class HarmonyApiClient {
1265
1300
  project_id: options.projectId,
1266
1301
  type: options.type?.length === 1 ? options.type[0] : undefined,
1267
1302
  limit: fetchLimit,
1303
+ include_episodes: options.includeEpisodes,
1268
1304
  },
1269
1305
  );
1270
1306
  entities = (search.entities ?? []) as Array<Record<string, unknown>>;
@@ -1277,6 +1313,7 @@ export class HarmonyApiClient {
1277
1313
  tags: options.tags,
1278
1314
  min_confidence: options.minConfidence,
1279
1315
  limit: fetchLimit,
1316
+ include_episodes: options.includeEpisodes,
1280
1317
  });
1281
1318
  entities = (list.entities ?? []) as Array<Record<string, unknown>>;
1282
1319
  }
@@ -1360,6 +1397,7 @@ export class HarmonyApiClient {
1360
1397
  limit?: number;
1361
1398
  tags?: string[];
1362
1399
  include_superseded?: boolean;
1400
+ include_episodes?: boolean;
1363
1401
  },
1364
1402
  ): Promise<{ entities: unknown[]; count: number }> {
1365
1403
  const params = new URLSearchParams();
@@ -1373,6 +1411,7 @@ export class HarmonyApiClient {
1373
1411
  // matches against the canonical `tags_normalized` column (#299).
1374
1412
  for (const tag of options?.tags ?? []) params.append("tags", tag);
1375
1413
  if (options?.include_superseded) params.set("include_superseded", "true");
1414
+ if (options?.include_episodes) params.set("include_episodes", "true");
1376
1415
  return this.request("GET", `/memory/search?${params.toString()}`);
1377
1416
  }
1378
1417
 
@@ -1383,12 +1422,14 @@ export class HarmonyApiClient {
1383
1422
  project_id?: string;
1384
1423
  type?: string;
1385
1424
  limit?: number;
1425
+ include_episodes?: boolean;
1386
1426
  }): Promise<{ entities: unknown[]; count: number }> {
1387
1427
  const params = new URLSearchParams();
1388
1428
  params.set("workspace_id", options.workspace_id);
1389
1429
  if (options.project_id) params.set("project_id", options.project_id);
1390
1430
  if (options.type) params.set("type", options.type);
1391
1431
  if (options.limit !== undefined) params.set("limit", String(options.limit));
1432
+ if (options.include_episodes) params.set("include_episodes", "true");
1392
1433
  return this.request("GET", `/memory/index?${params.toString()}`);
1393
1434
  }
1394
1435
 
@@ -1397,12 +1438,14 @@ export class HarmonyApiClient {
1397
1438
  project_id?: string;
1398
1439
  type?: string;
1399
1440
  limit?: number;
1441
+ include_episodes?: boolean;
1400
1442
  }): Promise<string> {
1401
1443
  const params = new URLSearchParams();
1402
1444
  params.set("workspace_id", options.workspace_id);
1403
1445
  if (options.project_id) params.set("project_id", options.project_id);
1404
1446
  if (options.type) params.set("type", options.type);
1405
1447
  if (options.limit !== undefined) params.set("limit", String(options.limit));
1448
+ if (options.include_episodes) params.set("include_episodes", "true");
1406
1449
  return this.requestRaw(
1407
1450
  "GET",
1408
1451
  `/memory/index?${params.toString()}`,
@@ -1489,6 +1532,7 @@ export class HarmonyApiClient {
1489
1532
  project_id?: string;
1490
1533
  type?: string;
1491
1534
  limit?: number;
1535
+ include_episodes?: boolean;
1492
1536
  },
1493
1537
  ): Promise<string> {
1494
1538
  const params = new URLSearchParams();
@@ -1498,6 +1542,7 @@ export class HarmonyApiClient {
1498
1542
  if (options?.type) params.set("type", options.type);
1499
1543
  if (options?.limit !== undefined)
1500
1544
  params.set("limit", String(options.limit));
1545
+ if (options?.include_episodes) params.set("include_episodes", "true");
1501
1546
  return this.requestRaw(
1502
1547
  "GET",
1503
1548
  `/memory/search?${params.toString()}`,
@@ -116,6 +116,27 @@ export const AUTO_START_TRIGGERS = new Set([
116
116
  export const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
117
117
  const CHECK_INTERVAL_MS = 60 * 1000; // 60 seconds
118
118
 
119
+ /**
120
+ * How long after the agent's last harmony tool call we keep heartbeating a
121
+ * session (card #696).
122
+ *
123
+ * WHY: the heartbeat asserts "this agent is still working". An *explicit*
124
+ * session (`/hmy`) is exempt from `checkInactivity`, so an unbounded heartbeat
125
+ * asserted that forever — bumping `updated_at` every 60s and starving the
126
+ * 30-minute `cleanup_stale_agent_sessions` cron, which is the only backstop for
127
+ * a session whose agent finished (card moved to Review) but never called
128
+ * `harmony_end_agent_session`. The session then showed "working" forever.
129
+ *
130
+ * Bounding the heartbeat by real activity restores that backstop: once the
131
+ * agent stops calling harmony tools we stop beating, `updated_at` goes stale,
132
+ * and the cron closes the row as `failed`/`stale`. Sits deliberately BELOW the
133
+ * cron's 30-minute threshold so a stalled session can actually reach it, and
134
+ * well above the milestone-gap the heartbeat exists to bridge (#608). Activity
135
+ * revives it — `trackActivity` refreshes `lastActivityAt` on every card-scoped
136
+ * tool call.
137
+ */
138
+ export const HEARTBEAT_ACTIVITY_WINDOW_MS = 25 * 60 * 1000; // 25 minutes
139
+
119
140
  /**
120
141
  * Scope used when no `scopeId` is supplied. stdio is single-user, so it shares
121
142
  * one scope; the unit tests also exercise this default scope exclusively.
@@ -429,22 +450,54 @@ export function noteSessionStatus(
429
450
  * `updated_at`. Covers both auto-started and explicit sessions (both live in
430
451
  * `scope.sessions`).
431
452
  *
453
+ * Two bounds keep a heartbeat from outliving the work it reports on (card #696):
454
+ *
455
+ * 1. `noCreate` — a heartbeat may only ever UPDATE a live session, never create
456
+ * one. The endpoint shares a single upsert with `start_agent_session`, so
457
+ * without this a heartbeat for a session ended out-of-band (the UI "Stop",
458
+ * which cannot reach this process's map) re-created the row and re-added the
459
+ * `agent` label. The #663 human-stop cooldown only deferred that by 10
460
+ * minutes, so a stopped card reliably respawned exactly one cooldown later.
461
+ * `session: null` back means the session is gone → untrack, stop beating.
462
+ * 2. `HEARTBEAT_ACTIVITY_WINDOW_MS` — only beat while the agent is still calling
463
+ * harmony tools, so an abandoned session goes stale and the cron can close it.
464
+ *
432
465
  * No double-heartbeat for the daemon: `packages/harmony-agent` has its own
433
466
  * progress-tracker heartbeat and never routes through mcp-server auto-sessions.
434
467
  */
435
468
  export function heartbeatActiveSessions(): void {
469
+ const now = Date.now();
436
470
  for (const scope of scopes.values()) {
437
471
  const client = scope.clientGetter?.();
438
472
  if (!client) continue;
439
- for (const session of scope.sessions.values()) {
473
+ // Snapshot: the untrack-on-ended path below mutates the map.
474
+ for (const [cardId, session] of [...scope.sessions.entries()]) {
440
475
  // Default-undefined status is treated as `working` (sessions created
441
476
  // before a status was reported).
442
477
  if ((session.status ?? "working") !== "working") continue;
478
+ // Silent agent → stop asserting it's working; hand the row to the cron.
479
+ if (now - session.lastActivityAt > HEARTBEAT_ACTIVITY_WINDOW_MS) continue;
443
480
  client
444
481
  .updateAgentProgress(session.cardId, {
445
482
  agentIdentifier: session.agentIdentifier,
446
483
  agentName: session.agentName,
447
484
  status: "working",
485
+ noCreate: true,
486
+ })
487
+ .then((res) => {
488
+ // Ended out-of-band (UI "Stop", another client, the stale cron). Drop
489
+ // it so we never beat — nor respawn — this session again. Only an
490
+ // explicit null counts: an unknown/absent field must not untrack a
491
+ // live session.
492
+ //
493
+ // Identity-checked: this reply describes the session we beat, which
494
+ // may no longer be the one tracked for this card. An end + restart
495
+ // landing inside the request's round-trip replaces the map entry, and
496
+ // deleting by cardId alone would drop that NEW live session — leaving
497
+ // it unbeaten until the stale cron closed it early.
498
+ if (res?.session === null && scope.sessions.get(cardId) === session) {
499
+ scope.sessions.delete(cardId);
500
+ }
448
501
  })
449
502
  .catch(() => {
450
503
  // Best-effort: a transient failure just defers the bump to the next
package/src/server.ts CHANGED
@@ -1743,6 +1743,11 @@ export const TOOLS = {
1743
1743
  description:
1744
1744
  "Include rows whose `superseded_at` is set (default false). Use when investigating supersede chains.",
1745
1745
  },
1746
+ include_episodes: {
1747
+ type: "boolean",
1748
+ description:
1749
+ "Include agent-run episodes — the daemon's per-run implement/review captures (default false). Excluded by default so recall surfaces curated, durable memory. Set true to audit the agent's run history.",
1750
+ },
1746
1751
  },
1747
1752
  },
1748
1753
  },
@@ -1934,6 +1939,11 @@ export const TOOLS = {
1934
1939
  description: "Project ID (optional)",
1935
1940
  },
1936
1941
  limit: { type: "number", description: "Max results (default: 20)" },
1942
+ include_episodes: {
1943
+ type: "boolean",
1944
+ description:
1945
+ "Include agent-run episodes — the daemon's per-run captures (default false). Excluded by default so search surfaces curated memory.",
1946
+ },
1937
1947
  },
1938
1948
  required: ["query"],
1939
1949
  },
@@ -1978,6 +1988,11 @@ export const TOOLS = {
1978
1988
  type: "number",
1979
1989
  description: "Max entities to return (default: 200)",
1980
1990
  },
1991
+ include_episodes: {
1992
+ type: "boolean",
1993
+ description:
1994
+ "Include agent-run episodes — the daemon's per-run captures (default false). Excluded by default so the index shows curated vault content, not run noise.",
1995
+ },
1981
1996
  },
1982
1997
  required: [],
1983
1998
  },
@@ -3474,6 +3489,42 @@ async function handleToolCall(
3474
3489
 
3475
3490
  case "harmony_set_workspace_context": {
3476
3491
  const workspaceId = z.string().uuid().parse(args.workspaceId);
3492
+
3493
+ // Fail fast on a workspace this connection can't actually reach (#695).
3494
+ // An OAuth token is bound at consent time to the single workspace the user
3495
+ // picked ("It will be able to read and modify cards, plans, and memory in
3496
+ // the workspace you select below"), and harmony-api enforces that on every
3497
+ // call. Setting the context is a pure local assignment, so without this
3498
+ // check it always "succeeded" and the caller only discovered the binding
3499
+ // when some later, unrelated tool call 403'd mid-task — the exact confusion
3500
+ // this card reports. `listWorkspaces` is server-side scoped to the bound
3501
+ // workspace, so presence in it is the authoritative reachability answer for
3502
+ // OAuth and legacy-key connections alike.
3503
+ const { workspaces } = await client.listWorkspaces();
3504
+ const available = (workspaces ?? []) as Array<{
3505
+ id?: string;
3506
+ name?: string;
3507
+ }>;
3508
+ const match = available.find((w) => w?.id === workspaceId);
3509
+
3510
+ if (!match) {
3511
+ const options = available
3512
+ .filter((w) => w?.id)
3513
+ .map((w) => ` - ${w.name ?? "(unnamed)"} (${w.id})`)
3514
+ .join("\n");
3515
+ throw new Error(
3516
+ `Workspace ${workspaceId} is not available to this connection.\n\n` +
3517
+ `This MCP connection is authorized for a specific workspace, chosen ` +
3518
+ `during OAuth consent — it cannot read or write any other workspace, ` +
3519
+ `even ones you're a member of.\n\n` +
3520
+ (options
3521
+ ? `Available here:\n${options}\n\n`
3522
+ : `No workspaces are available to this connection.\n\n`) +
3523
+ `To work in a different workspace, run /mcp to reconnect and select ` +
3524
+ `it on the consent screen.`,
3525
+ );
3526
+ }
3527
+
3477
3528
  deps.setActiveWorkspace(workspaceId);
3478
3529
  return { success: true, activeWorkspaceId: workspaceId };
3479
3530
  }
@@ -4108,6 +4159,8 @@ async function handleToolCall(
4108
4159
  const topK = args.top_k as number | undefined;
4109
4160
  const budgetTokens = args.budget_tokens as number | undefined;
4110
4161
  const includeSuperseded = Boolean(args.include_superseded);
4162
+ // #677: agent-run episodes are excluded from recall by default; opt in.
4163
+ const includeEpisodes = Boolean(args.include_episodes);
4111
4164
  const queryText = args.query as string | undefined;
4112
4165
 
4113
4166
  // Over-fetch when top_k is smaller than limit so the rescorer has
@@ -4166,6 +4219,7 @@ async function handleToolCall(
4166
4219
  ? requestedTags
4167
4220
  : undefined,
4168
4221
  include_superseded: includeSuperseded,
4222
+ include_episodes: includeEpisodes,
4169
4223
  },
4170
4224
  );
4171
4225
  entities = (searchResult.entities ?? []) as any[];
@@ -4199,6 +4253,7 @@ async function handleToolCall(
4199
4253
  min_confidence: args.minConfidence as number | undefined,
4200
4254
  limit: fetchLimit,
4201
4255
  include_superseded: includeSuperseded,
4256
+ include_episodes: includeEpisodes,
4202
4257
  });
4203
4258
  entities = listed as any[];
4204
4259
  if (excludeSessionFromLongTerm) {
@@ -4543,6 +4598,7 @@ async function handleToolCall(
4543
4598
  undefined,
4544
4599
  type: args.type as string | undefined,
4545
4600
  limit: args.limit as number | undefined,
4601
+ include_episodes: Boolean(args.include_episodes),
4546
4602
  },
4547
4603
  );
4548
4604
 
@@ -4570,6 +4626,7 @@ async function handleToolCall(
4570
4626
  (args.projectId as string) || deps.getActiveProjectId() || undefined,
4571
4627
  type: args.type as string | undefined,
4572
4628
  limit: args.limit as number | undefined,
4629
+ include_episodes: Boolean(args.include_episodes),
4573
4630
  });
4574
4631
  return markdown || "No entities found.";
4575
4632
  }