@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 +82 -9
- package/dist/index.js +82 -9
- package/dist/lib/api-client.js +32 -2
- package/package.json +1 -1
- package/src/api-client.ts +46 -1
- package/src/auto-session.ts +54 -1
- package/src/server.ts +57 -0
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.
|
|
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.
|
|
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
|
}
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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
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
|
-
|
|
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()}`,
|
package/src/auto-session.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|