@gethmy/mcp 2.20.1 → 2.22.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 +50 -9
- package/dist/index.js +50 -9
- package/dist/lib/api-client.js +8 -0
- package/package.json +4 -3
- package/src/api-client.ts +76 -5
- package/src/auto-session.ts +25 -6
- package/src/remote.ts +616 -266
- package/src/server.ts +96 -6
package/src/server.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./api-client.js";
|
|
20
20
|
import {
|
|
21
21
|
AUTO_START_TRIGGERS,
|
|
22
|
+
type ClientInfo,
|
|
22
23
|
destroyAutoSession,
|
|
23
24
|
initAutoSession,
|
|
24
25
|
markExplicit,
|
|
@@ -317,6 +318,18 @@ export interface ToolDeps {
|
|
|
317
318
|
* shared auto-session entry. Omitted on stdio (single user → default scope).
|
|
318
319
|
*/
|
|
319
320
|
getScopeId?: () => string;
|
|
321
|
+
/**
|
|
322
|
+
* MCP client identity for the caller, when the transport remembered one.
|
|
323
|
+
*
|
|
324
|
+
* A stateless `tools/call` carries no handshake, so `getClientVersion()` is
|
|
325
|
+
* undefined there and this is the only thing auto-session can attribute the
|
|
326
|
+
* work to (#297). Resolved per request, which makes it per *OAuth grant* on
|
|
327
|
+
* the hosted path (#774) — the scope-level `clientInfoGetter` cannot be,
|
|
328
|
+
* because a user's second client re-runs `initAutoSession` for the same scope
|
|
329
|
+
* and overwrites it (last-initializer-wins). Omitted on stdio, which reads
|
|
330
|
+
* identity straight off the in-scope `Server`.
|
|
331
|
+
*/
|
|
332
|
+
getClientInfo?: () => ClientInfo | null;
|
|
320
333
|
}
|
|
321
334
|
|
|
322
335
|
// --- Memory Session Tracking ---
|
|
@@ -532,8 +545,15 @@ async function flushMemoryActions(
|
|
|
532
545
|
agentIdentifier: session.agentIdentifier,
|
|
533
546
|
agentName: session.agentName,
|
|
534
547
|
recentActions: session.allActions,
|
|
548
|
+
// A bookkeeping flush of memory actions is the last thing that should mint
|
|
549
|
+
// a session on a card a human stopped (#770).
|
|
550
|
+
implicitCreate: true,
|
|
535
551
|
});
|
|
536
552
|
|
|
553
|
+
// Cleared even when the write was refused (`{session: null, stopped: true}`
|
|
554
|
+
// resolves rather than throwing). Deliberate: the bar never lapses, so these
|
|
555
|
+
// buffered display-only actions can never be written — retaining them would
|
|
556
|
+
// just re-attempt the same refusal on every later flush.
|
|
537
557
|
session.dirty = false;
|
|
538
558
|
} catch (err) {
|
|
539
559
|
// Fire-and-forget: log but don't propagate
|
|
@@ -1536,7 +1556,7 @@ export const TOOLS = {
|
|
|
1536
1556
|
},
|
|
1537
1557
|
harmony_update_agent_progress: {
|
|
1538
1558
|
description:
|
|
1539
|
-
"Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes.",
|
|
1559
|
+
"Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes. Check the reply: `session: null` with `stopped: true` means a human stopped this card's run, so reporting progress will no longer open a session here (card #770) — stop work if you were mid-run, and read `recovery` for the one legitimate way back. Otherwise a session is always returned: this call opens one if none is live.",
|
|
1540
1560
|
inputSchema: {
|
|
1541
1561
|
type: "object",
|
|
1542
1562
|
properties: {
|
|
@@ -1627,7 +1647,7 @@ export const TOOLS = {
|
|
|
1627
1647
|
},
|
|
1628
1648
|
harmony_get_pending_messages: {
|
|
1629
1649
|
description:
|
|
1630
|
-
"Drain queued steering messages a teammate sent to your live agent session (
|
|
1650
|
+
"Drain queued steering messages a teammate sent to your live agent session, and check whether that session is still live (cards #473, #770). Call at your progress checkpoints with the session id from harmony_start_agent_session and the highest seq you've already consumed; returns user messages with seq > sinceSeq, oldest first. Fold them into your next step and advance sinceSeq to the largest returned seq so each is handled exactly once. Two liveness flags come back and mean different things: `stopped` (a human pressed Stop) is TERMINAL — stop work at once, make no further edits, commits, pushes, or progress writes, don't move the card, and report what you had done and where any uncommitted work lives. `sessionStale` only means your session id went stale (usually the 30-minute inactivity cron) — nobody stopped you, so do NOT abandon the work: open a fresh session with harmony_start_agent_session and carry on with its new id. (Distinct from harmony_move_card's `sessionEnded`, which means the move deliberately closed your session.)",
|
|
1631
1651
|
inputSchema: {
|
|
1632
1652
|
type: "object",
|
|
1633
1653
|
properties: {
|
|
@@ -2614,11 +2634,15 @@ export function registerHandlers(server: Server, deps: ToolDeps): void {
|
|
|
2614
2634
|
// the hosted/OAuth path, not just stdio (card #297).
|
|
2615
2635
|
// Optional-chained: the pre-hook is best-effort and must never throw into
|
|
2616
2636
|
// tool dispatch if a transport/wrapper doesn't expose getClientVersion.
|
|
2637
|
+
// `getClientInfo` covers the stateless case, where the handshake this
|
|
2638
|
+
// reads is on a different request entirely (#774).
|
|
2617
2639
|
const cv = server.getClientVersion?.();
|
|
2618
2640
|
trackActivity(cardIdArg, {
|
|
2619
2641
|
autoStart: isAutoStartTrigger,
|
|
2620
2642
|
client: deps.getClient(),
|
|
2621
|
-
clientInfo: cv
|
|
2643
|
+
clientInfo: cv
|
|
2644
|
+
? { name: cv.name, version: cv.version }
|
|
2645
|
+
: (deps.getClientInfo?.() ?? undefined),
|
|
2622
2646
|
scopeId: deps.getScopeId?.(),
|
|
2623
2647
|
}).catch(() => {}); // fire-and-forget
|
|
2624
2648
|
}
|
|
@@ -2647,7 +2671,7 @@ export function registerHandlers(server: Server, deps: ToolDeps): void {
|
|
|
2647
2671
|
client: deps.getClient(),
|
|
2648
2672
|
clientInfo: cv
|
|
2649
2673
|
? { name: cv.name, version: cv.version }
|
|
2650
|
-
: undefined,
|
|
2674
|
+
: (deps.getClientInfo?.() ?? undefined),
|
|
2651
2675
|
scopeId: deps.getScopeId?.(),
|
|
2652
2676
|
}).catch(() => {});
|
|
2653
2677
|
}
|
|
@@ -3992,8 +4016,21 @@ async function handleToolCall(
|
|
|
3992
4016
|
),
|
|
3993
4017
|
...(mergedRecentActions && { recentActions: mergedRecentActions }),
|
|
3994
4018
|
...(runActivity.length > 0 && { runActivity }),
|
|
4019
|
+
// Reporting progress must never resurrect a run a human stopped (#770).
|
|
4020
|
+
// This is THE call the surviving client makes at every checkpoint, so it
|
|
4021
|
+
// is both the resurrection vector and the earliest place to hand that
|
|
4022
|
+
// client the news that it was stopped.
|
|
4023
|
+
implicitCreate: true,
|
|
3995
4024
|
});
|
|
3996
4025
|
|
|
4026
|
+
// This handler never sends `noCreate`, so the create branch either creates
|
|
4027
|
+
// a row or refuses — meaning a null session here can only be the human-stop
|
|
4028
|
+
// refusal. Drop it from auto-session tracking so the 60s heartbeat stops
|
|
4029
|
+
// beating a card we're barred from writing to.
|
|
4030
|
+
if (result.session === null) {
|
|
4031
|
+
untrack(cardId, deps.getScopeId?.());
|
|
4032
|
+
}
|
|
4033
|
+
|
|
3997
4034
|
// Phase 0 (memory architecture v2): mid-session learning extraction removed.
|
|
3998
4035
|
return { success: true, midSessionLearnings: 0, ...result };
|
|
3999
4036
|
}
|
|
@@ -4012,8 +4049,13 @@ async function handleToolCall(
|
|
|
4012
4049
|
await flushMemoryActions(client, cardId);
|
|
4013
4050
|
cleanupMemorySession(cardId);
|
|
4014
4051
|
|
|
4015
|
-
// End the session — tolerate failure (e.g., session already ended or not found)
|
|
4016
|
-
|
|
4052
|
+
// End the session — tolerate failure (e.g., session already ended or not found).
|
|
4053
|
+
// Typed off the client so the `ended`/`reason` discriminator (#769) reaches the
|
|
4054
|
+
// tool payload by contract, not by accident of the spread below. Left absent
|
|
4055
|
+
// here on purpose: a throw means we don't know what the server did.
|
|
4056
|
+
let result: Awaited<ReturnType<typeof client.endAgentSession>> = {
|
|
4057
|
+
session: null,
|
|
4058
|
+
};
|
|
4017
4059
|
let sessionEndError: string | null = null;
|
|
4018
4060
|
try {
|
|
4019
4061
|
result = await client.endAgentSession(cardId, {
|
|
@@ -4100,6 +4142,54 @@ async function handleToolCall(
|
|
|
4100
4142
|
sessionId,
|
|
4101
4143
|
sinceSeq,
|
|
4102
4144
|
);
|
|
4145
|
+
|
|
4146
|
+
// Hoist the verdict out of the nested `session` object and say what it
|
|
4147
|
+
// obliges (#770). The poll is the one call a stopped MCP/human client is
|
|
4148
|
+
// guaranteed to make — Stop's Realtime broadcast has no consumer for
|
|
4149
|
+
// `agent_id == null` sessions — so the news has to be impossible to skim
|
|
4150
|
+
// past here.
|
|
4151
|
+
//
|
|
4152
|
+
// Two outcomes, deliberately NOT merged. Only a human Stop is terminal; a
|
|
4153
|
+
// row the 30-minute stale cron closed (or one that vanished) just means the
|
|
4154
|
+
// session id is stale. Telling an agent to abandon its work in that case
|
|
4155
|
+
// would destroy a healthy run — a `blocked` session waiting on a user's
|
|
4156
|
+
// answer is not heartbeated, so it reaches the cron routinely.
|
|
4157
|
+
const stoppedByHuman = result.session?.stoppedByHuman === true;
|
|
4158
|
+
const ended = result.session?.ended === true;
|
|
4159
|
+
// Either verdict means stop beating this card: `ended` because the row is
|
|
4160
|
+
// gone, `stoppedByHuman` because we're about to tell the client to stand
|
|
4161
|
+
// down. Gating on `ended` alone would leave the 60s heartbeat polling a card
|
|
4162
|
+
// this very call declared terminal.
|
|
4163
|
+
if (ended || stoppedByHuman) {
|
|
4164
|
+
untrack(cardId, deps.getScopeId?.());
|
|
4165
|
+
}
|
|
4166
|
+
if (stoppedByHuman) {
|
|
4167
|
+
return {
|
|
4168
|
+
success: true,
|
|
4169
|
+
...result,
|
|
4170
|
+
stopped: true,
|
|
4171
|
+
stopReason: "human_stopped" as const,
|
|
4172
|
+
instruction:
|
|
4173
|
+
"A human stopped this run — stop work now. Make no further edits, commits, pushes, or progress writes, and do not move the card. Commit any uncommitted work in place so it isn't lost, then report to the user what you completed, what is unfinished, and the branch holding it.",
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
if (ended) {
|
|
4177
|
+
// Deliberately NOT `sessionEnded` — `harmony_move_card` already returns a
|
|
4178
|
+
// field by that name meaning "I ended your session for you", which is
|
|
4179
|
+
// close to the opposite of this. Two tools answering the same word with
|
|
4180
|
+
// near-inverse meanings is how a client ends up re-opening a session it
|
|
4181
|
+
// just intentionally closed.
|
|
4182
|
+
return {
|
|
4183
|
+
success: true,
|
|
4184
|
+
...result,
|
|
4185
|
+
sessionStale: true,
|
|
4186
|
+
staleReason: result.session?.status,
|
|
4187
|
+
instruction:
|
|
4188
|
+
'This session id is no longer live (it was closed as "' +
|
|
4189
|
+
(result.session?.status ?? "missing") +
|
|
4190
|
+
'") — but nobody stopped you. Do NOT abandon the work. Open a fresh session with harmony_start_agent_session, use its new id for later polls, and carry on.',
|
|
4191
|
+
};
|
|
4192
|
+
}
|
|
4103
4193
|
return { success: true, ...result };
|
|
4104
4194
|
}
|
|
4105
4195
|
|