@gethmy/mcp 2.14.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +62 -4
- package/dist/index.js +62 -4
- package/dist/lib/api-client.js +27 -0
- package/package.json +2 -2
- package/src/api-client.ts +64 -0
- package/src/auto-session.ts +91 -1
- package/src/server.ts +16 -6
package/dist/cli.js
CHANGED
|
@@ -1793,6 +1793,33 @@ class HarmonyApiClient {
|
|
|
1793
1793
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1794
1794
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
1795
1795
|
}
|
|
1796
|
+
async getFullBoard(projectId, options) {
|
|
1797
|
+
const { pageSize = 200, ...boardOpts } = options ?? {};
|
|
1798
|
+
const cards = [];
|
|
1799
|
+
let offset = 0;
|
|
1800
|
+
let last = null;
|
|
1801
|
+
for (;; ) {
|
|
1802
|
+
const page = await this.getBoard(projectId, {
|
|
1803
|
+
...boardOpts,
|
|
1804
|
+
limit: pageSize,
|
|
1805
|
+
offset
|
|
1806
|
+
});
|
|
1807
|
+
last = page;
|
|
1808
|
+
const pageCards = page.cards ?? [];
|
|
1809
|
+
cards.push(...pageCards);
|
|
1810
|
+
const hasMore = page.pagination?.hasMore ?? false;
|
|
1811
|
+
if (!hasMore || pageCards.length === 0)
|
|
1812
|
+
break;
|
|
1813
|
+
offset += pageSize;
|
|
1814
|
+
}
|
|
1815
|
+
return {
|
|
1816
|
+
project: last?.project,
|
|
1817
|
+
columns: last?.columns ?? [],
|
|
1818
|
+
cards,
|
|
1819
|
+
labels: last?.labels ?? [],
|
|
1820
|
+
totalCards: last?.pagination?.totalCards ?? cards.length
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1796
1823
|
async createCard(projectId, data) {
|
|
1797
1824
|
return this.request("POST", "/cards", { projectId, ...data });
|
|
1798
1825
|
}
|
|
@@ -2460,7 +2487,7 @@ function initAutoSession(callback, getClient2, getClientInfo, scopeId = DEFAULT_
|
|
|
2460
2487
|
scope.clientGetter = getClient2;
|
|
2461
2488
|
scope.clientInfoGetter = getClientInfo ?? null;
|
|
2462
2489
|
if (!inactivityTimer) {
|
|
2463
|
-
inactivityTimer = setInterval(
|
|
2490
|
+
inactivityTimer = setInterval(sweepTick, CHECK_INTERVAL_MS);
|
|
2464
2491
|
}
|
|
2465
2492
|
}
|
|
2466
2493
|
async function trackActivity(cardId, options) {
|
|
@@ -2502,7 +2529,8 @@ async function trackActivity(cardId, options) {
|
|
|
2502
2529
|
lastActivityAt: now,
|
|
2503
2530
|
isExplicit: false,
|
|
2504
2531
|
agentIdentifier,
|
|
2505
|
-
agentName
|
|
2532
|
+
agentName,
|
|
2533
|
+
status: "working"
|
|
2506
2534
|
});
|
|
2507
2535
|
}
|
|
2508
2536
|
function markExplicit(cardId, options) {
|
|
@@ -2521,7 +2549,8 @@ function markExplicit(cardId, options) {
|
|
|
2521
2549
|
lastActivityAt: Date.now(),
|
|
2522
2550
|
isExplicit: true,
|
|
2523
2551
|
agentIdentifier: options?.agentIdentifier ?? "explicit",
|
|
2524
|
-
agentName: options?.agentName ?? "Explicit Agent"
|
|
2552
|
+
agentName: options?.agentName ?? "Explicit Agent",
|
|
2553
|
+
status: "working"
|
|
2525
2554
|
});
|
|
2526
2555
|
}
|
|
2527
2556
|
}
|
|
@@ -2567,6 +2596,31 @@ function checkInactivity() {
|
|
|
2567
2596
|
}
|
|
2568
2597
|
}
|
|
2569
2598
|
}
|
|
2599
|
+
function noteSessionStatus(cardId, status, scopeId = DEFAULT_SCOPE) {
|
|
2600
|
+
const session = scopes.get(scopeId)?.sessions.get(cardId);
|
|
2601
|
+
if (session)
|
|
2602
|
+
session.status = status;
|
|
2603
|
+
}
|
|
2604
|
+
function heartbeatActiveSessions() {
|
|
2605
|
+
for (const scope of scopes.values()) {
|
|
2606
|
+
const client3 = scope.clientGetter?.();
|
|
2607
|
+
if (!client3)
|
|
2608
|
+
continue;
|
|
2609
|
+
for (const session of scope.sessions.values()) {
|
|
2610
|
+
if ((session.status ?? "working") !== "working")
|
|
2611
|
+
continue;
|
|
2612
|
+
client3.updateAgentProgress(session.cardId, {
|
|
2613
|
+
agentIdentifier: session.agentIdentifier,
|
|
2614
|
+
agentName: session.agentName,
|
|
2615
|
+
status: "working"
|
|
2616
|
+
}).catch(() => {});
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
function sweepTick() {
|
|
2621
|
+
checkInactivity();
|
|
2622
|
+
heartbeatActiveSessions();
|
|
2623
|
+
}
|
|
2570
2624
|
async function autoEndSession(scope, client3, cardId, status) {
|
|
2571
2625
|
if (!scope.sessions.delete(cardId))
|
|
2572
2626
|
return;
|
|
@@ -6203,10 +6257,14 @@ async function handleToolCall(name, args, deps) {
|
|
|
6203
6257
|
mergedRecentActions = callerRecentActions;
|
|
6204
6258
|
}
|
|
6205
6259
|
const runActivity = (callerActions || []).map((a) => a.description).filter((d) => typeof d === "string" && d.length > 0);
|
|
6260
|
+
const reportedStatus = args.status;
|
|
6261
|
+
if (reportedStatus) {
|
|
6262
|
+
noteSessionStatus(cardId, reportedStatus, deps.getScopeId?.());
|
|
6263
|
+
}
|
|
6206
6264
|
const result = await client3.updateAgentProgress(cardId, {
|
|
6207
6265
|
agentIdentifier,
|
|
6208
6266
|
agentName,
|
|
6209
|
-
status:
|
|
6267
|
+
status: reportedStatus,
|
|
6210
6268
|
progressPercent,
|
|
6211
6269
|
currentTask: args.currentTask,
|
|
6212
6270
|
blockers: args.blockers,
|
package/dist/index.js
CHANGED
|
@@ -1788,6 +1788,33 @@ class HarmonyApiClient {
|
|
|
1788
1788
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1789
1789
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
1790
1790
|
}
|
|
1791
|
+
async getFullBoard(projectId, options) {
|
|
1792
|
+
const { pageSize = 200, ...boardOpts } = options ?? {};
|
|
1793
|
+
const cards = [];
|
|
1794
|
+
let offset = 0;
|
|
1795
|
+
let last = null;
|
|
1796
|
+
for (;; ) {
|
|
1797
|
+
const page = await this.getBoard(projectId, {
|
|
1798
|
+
...boardOpts,
|
|
1799
|
+
limit: pageSize,
|
|
1800
|
+
offset
|
|
1801
|
+
});
|
|
1802
|
+
last = page;
|
|
1803
|
+
const pageCards = page.cards ?? [];
|
|
1804
|
+
cards.push(...pageCards);
|
|
1805
|
+
const hasMore = page.pagination?.hasMore ?? false;
|
|
1806
|
+
if (!hasMore || pageCards.length === 0)
|
|
1807
|
+
break;
|
|
1808
|
+
offset += pageSize;
|
|
1809
|
+
}
|
|
1810
|
+
return {
|
|
1811
|
+
project: last?.project,
|
|
1812
|
+
columns: last?.columns ?? [],
|
|
1813
|
+
cards,
|
|
1814
|
+
labels: last?.labels ?? [],
|
|
1815
|
+
totalCards: last?.pagination?.totalCards ?? cards.length
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1791
1818
|
async createCard(projectId, data) {
|
|
1792
1819
|
return this.request("POST", "/cards", { projectId, ...data });
|
|
1793
1820
|
}
|
|
@@ -2455,7 +2482,7 @@ function initAutoSession(callback, getClient2, getClientInfo, scopeId = DEFAULT_
|
|
|
2455
2482
|
scope.clientGetter = getClient2;
|
|
2456
2483
|
scope.clientInfoGetter = getClientInfo ?? null;
|
|
2457
2484
|
if (!inactivityTimer) {
|
|
2458
|
-
inactivityTimer = setInterval(
|
|
2485
|
+
inactivityTimer = setInterval(sweepTick, CHECK_INTERVAL_MS);
|
|
2459
2486
|
}
|
|
2460
2487
|
}
|
|
2461
2488
|
async function trackActivity(cardId, options) {
|
|
@@ -2497,7 +2524,8 @@ async function trackActivity(cardId, options) {
|
|
|
2497
2524
|
lastActivityAt: now,
|
|
2498
2525
|
isExplicit: false,
|
|
2499
2526
|
agentIdentifier,
|
|
2500
|
-
agentName
|
|
2527
|
+
agentName,
|
|
2528
|
+
status: "working"
|
|
2501
2529
|
});
|
|
2502
2530
|
}
|
|
2503
2531
|
function markExplicit(cardId, options) {
|
|
@@ -2516,7 +2544,8 @@ function markExplicit(cardId, options) {
|
|
|
2516
2544
|
lastActivityAt: Date.now(),
|
|
2517
2545
|
isExplicit: true,
|
|
2518
2546
|
agentIdentifier: options?.agentIdentifier ?? "explicit",
|
|
2519
|
-
agentName: options?.agentName ?? "Explicit Agent"
|
|
2547
|
+
agentName: options?.agentName ?? "Explicit Agent",
|
|
2548
|
+
status: "working"
|
|
2520
2549
|
});
|
|
2521
2550
|
}
|
|
2522
2551
|
}
|
|
@@ -2562,6 +2591,31 @@ function checkInactivity() {
|
|
|
2562
2591
|
}
|
|
2563
2592
|
}
|
|
2564
2593
|
}
|
|
2594
|
+
function noteSessionStatus(cardId, status, scopeId = DEFAULT_SCOPE) {
|
|
2595
|
+
const session = scopes.get(scopeId)?.sessions.get(cardId);
|
|
2596
|
+
if (session)
|
|
2597
|
+
session.status = status;
|
|
2598
|
+
}
|
|
2599
|
+
function heartbeatActiveSessions() {
|
|
2600
|
+
for (const scope of scopes.values()) {
|
|
2601
|
+
const client3 = scope.clientGetter?.();
|
|
2602
|
+
if (!client3)
|
|
2603
|
+
continue;
|
|
2604
|
+
for (const session of scope.sessions.values()) {
|
|
2605
|
+
if ((session.status ?? "working") !== "working")
|
|
2606
|
+
continue;
|
|
2607
|
+
client3.updateAgentProgress(session.cardId, {
|
|
2608
|
+
agentIdentifier: session.agentIdentifier,
|
|
2609
|
+
agentName: session.agentName,
|
|
2610
|
+
status: "working"
|
|
2611
|
+
}).catch(() => {});
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
function sweepTick() {
|
|
2616
|
+
checkInactivity();
|
|
2617
|
+
heartbeatActiveSessions();
|
|
2618
|
+
}
|
|
2565
2619
|
async function autoEndSession(scope, client3, cardId, status) {
|
|
2566
2620
|
if (!scope.sessions.delete(cardId))
|
|
2567
2621
|
return;
|
|
@@ -6198,10 +6252,14 @@ async function handleToolCall(name, args, deps) {
|
|
|
6198
6252
|
mergedRecentActions = callerRecentActions;
|
|
6199
6253
|
}
|
|
6200
6254
|
const runActivity = (callerActions || []).map((a) => a.description).filter((d) => typeof d === "string" && d.length > 0);
|
|
6255
|
+
const reportedStatus = args.status;
|
|
6256
|
+
if (reportedStatus) {
|
|
6257
|
+
noteSessionStatus(cardId, reportedStatus, deps.getScopeId?.());
|
|
6258
|
+
}
|
|
6201
6259
|
const result = await client3.updateAgentProgress(cardId, {
|
|
6202
6260
|
agentIdentifier,
|
|
6203
6261
|
agentName,
|
|
6204
|
-
status:
|
|
6262
|
+
status: reportedStatus,
|
|
6205
6263
|
progressPercent,
|
|
6206
6264
|
currentTask: args.currentTask,
|
|
6207
6265
|
blockers: args.blockers,
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1240,6 +1240,33 @@ class HarmonyApiClient {
|
|
|
1240
1240
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1241
1241
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
1242
1242
|
}
|
|
1243
|
+
async getFullBoard(projectId, options) {
|
|
1244
|
+
const { pageSize = 200, ...boardOpts } = options ?? {};
|
|
1245
|
+
const cards = [];
|
|
1246
|
+
let offset = 0;
|
|
1247
|
+
let last = null;
|
|
1248
|
+
for (;; ) {
|
|
1249
|
+
const page = await this.getBoard(projectId, {
|
|
1250
|
+
...boardOpts,
|
|
1251
|
+
limit: pageSize,
|
|
1252
|
+
offset
|
|
1253
|
+
});
|
|
1254
|
+
last = page;
|
|
1255
|
+
const pageCards = page.cards ?? [];
|
|
1256
|
+
cards.push(...pageCards);
|
|
1257
|
+
const hasMore = page.pagination?.hasMore ?? false;
|
|
1258
|
+
if (!hasMore || pageCards.length === 0)
|
|
1259
|
+
break;
|
|
1260
|
+
offset += pageSize;
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
project: last?.project,
|
|
1264
|
+
columns: last?.columns ?? [],
|
|
1265
|
+
cards,
|
|
1266
|
+
labels: last?.labels ?? [],
|
|
1267
|
+
totalCards: last?.pagination?.totalCards ?? cards.length
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1243
1270
|
async createCard(projectId, data) {
|
|
1244
1271
|
return this.request("POST", "/cards", { projectId, ...data });
|
|
1245
1272
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"serve:remote": "bun src/remote.ts",
|
|
65
65
|
"dev": "bun --watch src/index.ts",
|
|
66
66
|
"test": "bun run test:unit && bun run test:integration",
|
|
67
|
-
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts src/__tests__/skills.test.ts src/__tests__/hmy-config.test.ts src/__tests__/tool-dispatch.test.ts src/__tests__/mcp-integration.test.ts",
|
|
67
|
+
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts src/__tests__/skills.test.ts src/__tests__/hmy-config.test.ts src/__tests__/tool-dispatch.test.ts src/__tests__/mcp-integration.test.ts src/__tests__/auto-session.test.ts",
|
|
68
68
|
"test:integration": "bun test src/__tests__/integration-memory-system.test.ts src/__tests__/integration-memory-crud.test.ts",
|
|
69
69
|
"typecheck": "tsc --noEmit",
|
|
70
70
|
"prepublishOnly": "bun run typecheck && bun run build"
|
package/src/api-client.ts
CHANGED
|
@@ -527,6 +527,70 @@ export class HarmonyApiClient {
|
|
|
527
527
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Fetch the FULL board — every non-archived card — by paginating past the
|
|
532
|
+
* server's default 50-card window (`getBoard` caps at `limit=50` to keep MCP
|
|
533
|
+
* responses under the token budget).
|
|
534
|
+
*
|
|
535
|
+
* Daemon board scans must see the whole board: with the 50-card cap the
|
|
536
|
+
* entire `Review` column fell outside the first-50-by-position window once a
|
|
537
|
+
* project crossed 50 cards, silently starving the review pipeline (#628 — a
|
|
538
|
+
* 272-card project had 0 reviews for ~2 days). This has no LLM-context/token
|
|
539
|
+
* concern daemon-side, so it walks `offset` until `pagination.hasMore` is
|
|
540
|
+
* false and concatenates every page's cards.
|
|
541
|
+
*
|
|
542
|
+
* Summary mode carries no `cards` array, so callers that only need per-column
|
|
543
|
+
* counts should keep using `getBoard({ summary: true })`. This is the
|
|
544
|
+
* card-list path.
|
|
545
|
+
*/
|
|
546
|
+
async getFullBoard(
|
|
547
|
+
projectId: string,
|
|
548
|
+
options?: {
|
|
549
|
+
columnId?: string;
|
|
550
|
+
includeArchived?: boolean;
|
|
551
|
+
labelName?: string;
|
|
552
|
+
/** Page size for the offset walk. Defaults to 200. */
|
|
553
|
+
pageSize?: number;
|
|
554
|
+
},
|
|
555
|
+
): Promise<{
|
|
556
|
+
project: unknown;
|
|
557
|
+
columns: unknown[];
|
|
558
|
+
cards: unknown[];
|
|
559
|
+
labels: unknown[];
|
|
560
|
+
totalCards: number;
|
|
561
|
+
}> {
|
|
562
|
+
const { pageSize = 200, ...boardOpts } = options ?? {};
|
|
563
|
+
const cards: unknown[] = [];
|
|
564
|
+
let offset = 0;
|
|
565
|
+
let last: Awaited<ReturnType<HarmonyApiClient["getBoard"]>> | null = null;
|
|
566
|
+
|
|
567
|
+
// Walk pages until the server reports no more. Guard on `hasMore` (not
|
|
568
|
+
// `cards.length`) so a label-filtered final page shorter than pageSize
|
|
569
|
+
// terminates cleanly; the empty-page break is a belt-and-braces backstop
|
|
570
|
+
// against a server that omits `pagination`.
|
|
571
|
+
for (;;) {
|
|
572
|
+
const page = await this.getBoard(projectId, {
|
|
573
|
+
...boardOpts,
|
|
574
|
+
limit: pageSize,
|
|
575
|
+
offset,
|
|
576
|
+
});
|
|
577
|
+
last = page;
|
|
578
|
+
const pageCards = page.cards ?? [];
|
|
579
|
+
cards.push(...pageCards);
|
|
580
|
+
const hasMore = page.pagination?.hasMore ?? false;
|
|
581
|
+
if (!hasMore || pageCards.length === 0) break;
|
|
582
|
+
offset += pageSize;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
project: last?.project,
|
|
587
|
+
columns: last?.columns ?? [],
|
|
588
|
+
cards,
|
|
589
|
+
labels: last?.labels ?? [],
|
|
590
|
+
totalCards: last?.pagination?.totalCards ?? cards.length,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
530
594
|
// ============ CARD OPERATIONS ============
|
|
531
595
|
|
|
532
596
|
async createCard(
|
package/src/auto-session.ts
CHANGED
|
@@ -25,6 +25,16 @@
|
|
|
25
25
|
|
|
26
26
|
import type { HarmonyApiClient } from "./api-client.js";
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Status reported for a tracked session. Drives the heartbeat decision: only
|
|
30
|
+
* `working` sessions are heartbeated by the sweep. `paused`/`blocked`/`waiting`
|
|
31
|
+
* are intentional non-heartbeat states — the agent has deliberately stopped
|
|
32
|
+
* making progress (parked for a human, awaiting input, …), so bumping their
|
|
33
|
+
* `updated_at` would both keep a paused row looking active and mask a genuinely
|
|
34
|
+
* stalled run.
|
|
35
|
+
*/
|
|
36
|
+
export type SessionStatus = "working" | "blocked" | "waiting" | "paused";
|
|
37
|
+
|
|
28
38
|
export interface TrackedSession {
|
|
29
39
|
cardId: string;
|
|
30
40
|
startedAt: number;
|
|
@@ -33,6 +43,11 @@ export interface TrackedSession {
|
|
|
33
43
|
isExplicit: boolean;
|
|
34
44
|
agentIdentifier: string;
|
|
35
45
|
agentName: string;
|
|
46
|
+
/**
|
|
47
|
+
* Last status reported for the session (default `working`). Only `working`
|
|
48
|
+
* sessions are heartbeated by the sweep; see `heartbeatActiveSessions`.
|
|
49
|
+
*/
|
|
50
|
+
status?: SessionStatus;
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
export type EndSessionCallback = (
|
|
@@ -173,7 +188,7 @@ export function initAutoSession(
|
|
|
173
188
|
// the remote transport, connections arrive faster than the 60s interval and a
|
|
174
189
|
// restart-each-time would keep deferring the sweep indefinitely (starvation).
|
|
175
190
|
if (!inactivityTimer) {
|
|
176
|
-
inactivityTimer = setInterval(
|
|
191
|
+
inactivityTimer = setInterval(sweepTick, CHECK_INTERVAL_MS);
|
|
177
192
|
}
|
|
178
193
|
}
|
|
179
194
|
|
|
@@ -256,6 +271,7 @@ export async function trackActivity(
|
|
|
256
271
|
isExplicit: false,
|
|
257
272
|
agentIdentifier,
|
|
258
273
|
agentName,
|
|
274
|
+
status: "working",
|
|
259
275
|
});
|
|
260
276
|
}
|
|
261
277
|
|
|
@@ -283,6 +299,7 @@ export function markExplicit(
|
|
|
283
299
|
isExplicit: true,
|
|
284
300
|
agentIdentifier: options?.agentIdentifier ?? "explicit",
|
|
285
301
|
agentName: options?.agentName ?? "Explicit Agent",
|
|
302
|
+
status: "working",
|
|
286
303
|
});
|
|
287
304
|
}
|
|
288
305
|
}
|
|
@@ -376,6 +393,79 @@ export function checkInactivity(): void {
|
|
|
376
393
|
}
|
|
377
394
|
}
|
|
378
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Record the latest status reported for a tracked session so the sweep knows
|
|
398
|
+
* whether to keep heartbeating it. Wired into `harmony_update_agent_progress`
|
|
399
|
+
* (server.ts): when an agent reports `paused`/`blocked`/`waiting` the heartbeat
|
|
400
|
+
* stops for that session (those are intentional non-heartbeat states); a later
|
|
401
|
+
* `working` report resumes it. No-op when the card isn't tracked in the scope.
|
|
402
|
+
*/
|
|
403
|
+
export function noteSessionStatus(
|
|
404
|
+
cardId: string,
|
|
405
|
+
status: SessionStatus,
|
|
406
|
+
scopeId: string = DEFAULT_SCOPE,
|
|
407
|
+
): void {
|
|
408
|
+
const session = scopes.get(scopeId)?.sessions.get(cardId);
|
|
409
|
+
if (session) session.status = status;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Heartbeat every tracked `working` session by writing a status-only
|
|
414
|
+
* `updateAgentProgress`. The backend's `card_agent_context` update trigger
|
|
415
|
+
* bumps `updated_at` — the field both the kanban card ribbon
|
|
416
|
+
* (`isAgentContextLive`) and the header "Connected Agents" panel key off to
|
|
417
|
+
* decide a session is still live.
|
|
418
|
+
*
|
|
419
|
+
* WHY (card #608): an interactive MCP client (Claude Code, Cursor, /hmy) opens a
|
|
420
|
+
* `card_agent_context` row but, unlike the agent daemon's progress-tracker (60s
|
|
421
|
+
* heartbeat), only writes progress at milestones — often many minutes apart. On
|
|
422
|
+
* a long run `updated_at` goes stale and the card drops its live indicator
|
|
423
|
+
* mid-flight. This sweep gives interactive sessions the same ~60s heartbeat.
|
|
424
|
+
*
|
|
425
|
+
* Only `working` sessions are heartbeated; `paused`/`blocked`/`waiting` are
|
|
426
|
+
* intentional non-heartbeat states (see `SessionStatus`). The payload is
|
|
427
|
+
* status-only (no currentTask/progressPercent) so the heartbeat never clobbers
|
|
428
|
+
* the real progress the agent last reported — the trigger still bumps
|
|
429
|
+
* `updated_at`. Covers both auto-started and explicit sessions (both live in
|
|
430
|
+
* `scope.sessions`).
|
|
431
|
+
*
|
|
432
|
+
* No double-heartbeat for the daemon: `packages/harmony-agent` has its own
|
|
433
|
+
* progress-tracker heartbeat and never routes through mcp-server auto-sessions.
|
|
434
|
+
*/
|
|
435
|
+
export function heartbeatActiveSessions(): void {
|
|
436
|
+
for (const scope of scopes.values()) {
|
|
437
|
+
const client = scope.clientGetter?.();
|
|
438
|
+
if (!client) continue;
|
|
439
|
+
for (const session of scope.sessions.values()) {
|
|
440
|
+
// Default-undefined status is treated as `working` (sessions created
|
|
441
|
+
// before a status was reported).
|
|
442
|
+
if ((session.status ?? "working") !== "working") continue;
|
|
443
|
+
client
|
|
444
|
+
.updateAgentProgress(session.cardId, {
|
|
445
|
+
agentIdentifier: session.agentIdentifier,
|
|
446
|
+
agentName: session.agentName,
|
|
447
|
+
status: "working",
|
|
448
|
+
})
|
|
449
|
+
.catch(() => {
|
|
450
|
+
// Best-effort: a transient failure just defers the bump to the next
|
|
451
|
+
// sweep. updateAgentProgress is fire-and-forget here.
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* One tick of the shared 60s sweep: prune idle auto-sessions
|
|
459
|
+
* (`checkInactivity`) first, then heartbeat the surviving `working` sessions
|
|
460
|
+
* (`heartbeatActiveSessions`). Order matters — prune before heartbeat so we
|
|
461
|
+
* never bump a session we're about to end. Exported for tests; in production
|
|
462
|
+
* the single process-wide timer calls it every CHECK_INTERVAL_MS.
|
|
463
|
+
*/
|
|
464
|
+
export function sweepTick(): void {
|
|
465
|
+
checkInactivity();
|
|
466
|
+
heartbeatActiveSessions();
|
|
467
|
+
}
|
|
468
|
+
|
|
379
469
|
// --- Internal ---
|
|
380
470
|
|
|
381
471
|
async function autoEndSession(
|
package/src/server.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
destroyAutoSession,
|
|
23
23
|
initAutoSession,
|
|
24
24
|
markExplicit,
|
|
25
|
+
noteSessionStatus,
|
|
25
26
|
shutdownAllSessions,
|
|
26
27
|
trackActivity,
|
|
27
28
|
untrack,
|
|
@@ -3684,15 +3685,24 @@ async function handleToolCall(
|
|
|
3684
3685
|
.map((a) => a.description)
|
|
3685
3686
|
.filter((d): d is string => typeof d === "string" && d.length > 0);
|
|
3686
3687
|
|
|
3688
|
+
const reportedStatus = args.status as
|
|
3689
|
+
| "working"
|
|
3690
|
+
| "blocked"
|
|
3691
|
+
| "waiting"
|
|
3692
|
+
| "paused"
|
|
3693
|
+
| undefined;
|
|
3694
|
+
|
|
3695
|
+
// Keep the auto-session heartbeat in step with the agent's own status
|
|
3696
|
+
// reports: `paused`/`blocked`/`waiting` stop the 60s heartbeat (intentional
|
|
3697
|
+
// non-heartbeat states), a later `working` resumes it (card #608).
|
|
3698
|
+
if (reportedStatus) {
|
|
3699
|
+
noteSessionStatus(cardId, reportedStatus, deps.getScopeId?.());
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3687
3702
|
const result = await client.updateAgentProgress(cardId, {
|
|
3688
3703
|
agentIdentifier,
|
|
3689
3704
|
agentName,
|
|
3690
|
-
status:
|
|
3691
|
-
| "working"
|
|
3692
|
-
| "blocked"
|
|
3693
|
-
| "waiting"
|
|
3694
|
-
| "paused"
|
|
3695
|
-
| undefined,
|
|
3705
|
+
status: reportedStatus,
|
|
3696
3706
|
progressPercent,
|
|
3697
3707
|
currentTask: args.currentTask as string | undefined,
|
|
3698
3708
|
blockers: args.blockers as string[] | undefined,
|