@opengsd/gsd-pi 1.4.0-dev.5fc22c6f → 1.4.0-dev.c18009cd
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/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +58 -4
- package/dist/resources/extensions/gsd/artifact-verification.js +17 -5
- package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
- package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
- package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
- package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
- package/dist/resources/extensions/gsd/auto.js +10 -3
- package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +174 -16
- package/dist/resources/extensions/gsd/gsd-db.js +52 -0
- package/dist/resources/extensions/gsd/migrate-external.js +13 -4
- package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
- package/dist/resources/extensions/gsd/repo-identity.js +16 -0
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
- package/dist/resources/extensions/gsd/unit-registry.js +27 -2
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/integrations/hermes/CONTEXT.md +47 -0
- package/integrations/hermes/README.md +21 -0
- package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
- package/integrations/hermes/docs/setup.md +4 -0
- package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
- package/integrations/hermes/open_gsd_hermes/config.py +2 -0
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
- package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
- package/integrations/hermes/plugin.yaml +4 -0
- package/integrations/hermes/tests/test_config.py +6 -0
- package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
- package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
- package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
- package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
- package/integrations/hermes/tests/test_milestone_client.py +447 -0
- package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
- package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/session-manager.js +1 -1
- package/packages/daemon/dist/session-manager.js.map +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/server.d.ts +18 -5
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +46 -5
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +1 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +64 -3
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +212 -39
- package/src/resources/extensions/gsd/artifact-verification.ts +16 -4
- package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
- package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
- package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
- package/src/resources/extensions/gsd/auto.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +230 -21
- package/src/resources/extensions/gsd/gsd-db.ts +72 -0
- package/src/resources/extensions/gsd/migrate-external.ts +14 -4
- package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
- package/src/resources/extensions/gsd/repo-identity.ts +17 -0
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +335 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
- package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
- package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
- package/src/resources/extensions/gsd/unit-registry.ts +34 -2
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_ssgManifest.js +0 -0
package/pkg/package.json
CHANGED
|
@@ -50,6 +50,7 @@ import { markToolStart, markToolEnd } from "../gsd/auto.js";
|
|
|
50
50
|
import {
|
|
51
51
|
markInteractiveElicitationStart,
|
|
52
52
|
markInteractiveElicitationEnd,
|
|
53
|
+
isInteractiveElicitationInFlight,
|
|
53
54
|
} from "../gsd/auto-tool-tracking.js";
|
|
54
55
|
import {
|
|
55
56
|
discoverBrowserMcpServerName,
|
|
@@ -227,6 +228,36 @@ interface SDKInputUserMessage {
|
|
|
227
228
|
const OTHER_OPTION_LABEL = "None of the above";
|
|
228
229
|
/** Regex pattern that identifies field names and descriptions that should be treated as sensitive/secure inputs. */
|
|
229
230
|
const SENSITIVE_FIELD_PATTERN = /(password|passphrase|secret|token|api[_\s-]*key|private[_\s-]*key|credential)/i;
|
|
231
|
+
/** Mirrors @opengsd/mcp-server's ask_user_questions host elicitation timeout. */
|
|
232
|
+
const WORKFLOW_ELICIT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
233
|
+
/** Close the local form just before the server drops the MCP elicitation request. */
|
|
234
|
+
const WORKFLOW_ELICIT_TIMEOUT_SKEW_MS = 1_000;
|
|
235
|
+
export const CLAUDE_CODE_INTERVIEW_FORM_TIMEOUT_MS = Math.max(
|
|
236
|
+
1,
|
|
237
|
+
WORKFLOW_ELICIT_TIMEOUT_MS - WORKFLOW_ELICIT_TIMEOUT_SKEW_MS,
|
|
238
|
+
);
|
|
239
|
+
const ELICITATION_EXPIRED_NOTICE =
|
|
240
|
+
"Question expired before you answered - the form was closed and no answers were sent.";
|
|
241
|
+
const activeAskUserQuestionElicitationTimeoutAborters = new Set<() => void>();
|
|
242
|
+
|
|
243
|
+
function isAskUserQuestionsToolName(toolName: string | undefined): boolean {
|
|
244
|
+
const name = String(toolName ?? "");
|
|
245
|
+
return name === "ask_user_questions" || name.endsWith("__ask_user_questions");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isAskUserQuestionsTimedOutResult(
|
|
249
|
+
toolCall: Pick<ToolCall, "name">,
|
|
250
|
+
result: ExternalToolResultPayload,
|
|
251
|
+
): boolean {
|
|
252
|
+
return isAskUserQuestionsToolName(toolCall.name) && result.details?.timed_out === true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function abortActiveAskUserQuestionElicitationsForTimeout(): void {
|
|
256
|
+
if (!isInteractiveElicitationInFlight()) return;
|
|
257
|
+
for (const abort of [...activeAskUserQuestionElicitationTimeoutAborters]) {
|
|
258
|
+
abort();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
230
261
|
|
|
231
262
|
// ---------------------------------------------------------------------------
|
|
232
263
|
// Stream factory
|
|
@@ -1566,12 +1597,32 @@ export function createClaudeCodeElicitationHandler(
|
|
|
1566
1597
|
// it does not tear down the very elicitation that IS the human boundary
|
|
1567
1598
|
// (#cc-elicitation-self-cancel). It clears in the finally on every exit.
|
|
1568
1599
|
const elicId = "cc-elicit-" + ((request as { id?: string | number }).id ?? `${Date.now()}-${nextElicitationSeq()}`);
|
|
1600
|
+
const formAbortController = new AbortController();
|
|
1601
|
+
let expiryNoticeShown = false;
|
|
1602
|
+
const abortExpiredForm = (): void => {
|
|
1603
|
+
if (formAbortController.signal.aborted) return;
|
|
1604
|
+
formAbortController.abort();
|
|
1605
|
+
if (!expiryNoticeShown) {
|
|
1606
|
+
expiryNoticeShown = true;
|
|
1607
|
+
ui.notify(ELICITATION_EXPIRED_NOTICE, "warning");
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
const forwardSdkAbort = (): void => {
|
|
1611
|
+
if (!formAbortController.signal.aborted) formAbortController.abort();
|
|
1612
|
+
};
|
|
1613
|
+
if (signal.aborted) {
|
|
1614
|
+
forwardSdkAbort();
|
|
1615
|
+
} else {
|
|
1616
|
+
signal.addEventListener("abort", forwardSdkAbort, { once: true });
|
|
1617
|
+
}
|
|
1618
|
+
const expiryTimer = setTimeout(abortExpiredForm, CLAUDE_CODE_INTERVIEW_FORM_TIMEOUT_MS);
|
|
1569
1619
|
markInteractiveElicitationStart();
|
|
1570
1620
|
markToolStart(elicId, "ask_user_questions");
|
|
1621
|
+
activeAskUserQuestionElicitationTimeoutAborters.add(abortExpiredForm);
|
|
1571
1622
|
try {
|
|
1572
1623
|
const interviewResult = await showInterviewRound(
|
|
1573
1624
|
questions,
|
|
1574
|
-
{ signal, overlay: true },
|
|
1625
|
+
{ signal: formAbortController.signal, overlay: true },
|
|
1575
1626
|
{ ui } as any,
|
|
1576
1627
|
).catch(() => undefined);
|
|
1577
1628
|
if (interviewResult === undefined) {
|
|
@@ -1580,7 +1631,7 @@ export function createClaudeCodeElicitationHandler(
|
|
|
1580
1631
|
// `finally` runs the moment the promise is created and the fallback
|
|
1581
1632
|
// wait runs with zero in-flight tools — reintroducing the
|
|
1582
1633
|
// self-cancel on this path (Bugbot #1c00624d).
|
|
1583
|
-
return await promptElicitationWithDialogs(request, questions, ui, signal);
|
|
1634
|
+
return await promptElicitationWithDialogs(request, questions, ui, formAbortController.signal);
|
|
1584
1635
|
}
|
|
1585
1636
|
if (Object.keys(interviewResult.answers).length === 0) {
|
|
1586
1637
|
// A system/host teardown (compaction, session_switch, true
|
|
@@ -1596,6 +1647,9 @@ export function createClaudeCodeElicitationHandler(
|
|
|
1596
1647
|
content: roundResultToElicitationContent(questions, interviewResult),
|
|
1597
1648
|
};
|
|
1598
1649
|
} finally {
|
|
1650
|
+
clearTimeout(expiryTimer);
|
|
1651
|
+
signal.removeEventListener("abort", forwardSdkAbort);
|
|
1652
|
+
activeAskUserQuestionElicitationTimeoutAborters.delete(abortExpiredForm);
|
|
1599
1653
|
markToolEnd(elicId);
|
|
1600
1654
|
markInteractiveElicitationEnd();
|
|
1601
1655
|
}
|
|
@@ -2526,6 +2580,9 @@ async function pumpSdkMessages(
|
|
|
2526
2580
|
// Push synthetic completion events with result attached so the
|
|
2527
2581
|
// chat-controller can update pending ToolExecutionComponents.
|
|
2528
2582
|
if (block.type === "toolCall") {
|
|
2583
|
+
if (isAskUserQuestionsTimedOutResult(block, extResult)) {
|
|
2584
|
+
abortActiveAskUserQuestionElicitationsForTimeout();
|
|
2585
|
+
}
|
|
2529
2586
|
if (suppressDuplicateUnavailable) {
|
|
2530
2587
|
delete (block as ToolCallWithExternalResult).externalResult;
|
|
2531
2588
|
stream.push({
|
|
@@ -2554,9 +2611,13 @@ async function pumpSdkMessages(
|
|
|
2554
2611
|
});
|
|
2555
2612
|
emittedExternalToolResultIds.add(block.id);
|
|
2556
2613
|
} else if (block.type === "serverToolUse") {
|
|
2614
|
+
const toolCall = serverToolUseToToolCallLike(block);
|
|
2615
|
+
if (isAskUserQuestionsTimedOutResult(toolCall, extResult)) {
|
|
2616
|
+
abortActiveAskUserQuestionElicitationsForTimeout();
|
|
2617
|
+
}
|
|
2557
2618
|
try {
|
|
2558
2619
|
await onExternalToolResult?.({
|
|
2559
|
-
toolCall
|
|
2620
|
+
toolCall,
|
|
2560
2621
|
result: extResult,
|
|
2561
2622
|
});
|
|
2562
2623
|
} catch (error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// gsd-pi - Claude Code stream adapter regression tests
|
|
2
|
-
import { describe, test } from "node:test";
|
|
2
|
+
import { describe, mock, test } from "node:test";
|
|
3
3
|
import { clearGuidedUnitContext, setGuidedUnitContext } from "../../gsd/guided-unit-context.ts";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
5
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
buildWorkflowMcpReadinessProgressMessage,
|
|
47
47
|
pushWorkflowMcpReadinessProgressEvent,
|
|
48
48
|
resolveWorkflowMcpPreflightServerConfig,
|
|
49
|
+
CLAUDE_CODE_INTERVIEW_FORM_TIMEOUT_MS,
|
|
49
50
|
} from "../stream-adapter.ts";
|
|
50
51
|
import { CLAUDE_CODE_MODELS } from "../models.ts";
|
|
51
52
|
import type { AssistantMessage, Context, Message } from "@gsd/pi-ai";
|
|
@@ -107,6 +108,51 @@ function setWorkflowMcpEnv(
|
|
|
107
108
|
};
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Shared MCP elicitation fixture — a representative ask_user_questions
|
|
113
|
+
// elicitation request. Used both by the external-tool-results interview
|
|
114
|
+
// lifecycle tests and by the MCP elicitation bridge tests, so it lives at
|
|
115
|
+
// module scope.
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
const askUserQuestionsRequest = {
|
|
119
|
+
serverName: "gsd-workflow",
|
|
120
|
+
message: "Please answer the following question(s).",
|
|
121
|
+
mode: "form" as const,
|
|
122
|
+
requestedSchema: {
|
|
123
|
+
type: "object" as const,
|
|
124
|
+
properties: {
|
|
125
|
+
storage_scope: {
|
|
126
|
+
type: "string",
|
|
127
|
+
title: "Storage",
|
|
128
|
+
description: "Does this app need to sync across devices?",
|
|
129
|
+
oneOf: [
|
|
130
|
+
{ const: "Local-only (Recommended)", title: "Local-only (Recommended)" },
|
|
131
|
+
{ const: "Cloud-synced", title: "Cloud-synced" },
|
|
132
|
+
{ const: "None of the above", title: "None of the above" },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
storage_scope__note: {
|
|
136
|
+
type: "string",
|
|
137
|
+
title: "Storage Note",
|
|
138
|
+
description: "Optional note for None of the above.",
|
|
139
|
+
},
|
|
140
|
+
platform: {
|
|
141
|
+
type: "array",
|
|
142
|
+
title: "Platform",
|
|
143
|
+
description: "Where should it run?",
|
|
144
|
+
items: {
|
|
145
|
+
anyOf: [
|
|
146
|
+
{ const: "Web", title: "Web" },
|
|
147
|
+
{ const: "Desktop", title: "Desktop" },
|
|
148
|
+
{ const: "Mobile", title: "Mobile" },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
110
156
|
// ---------------------------------------------------------------------------
|
|
111
157
|
// Existing tests — exhausted stream fallback (#2575)
|
|
112
158
|
// ---------------------------------------------------------------------------
|
|
@@ -820,6 +866,134 @@ describe("stream-adapter — Claude Code external tool results", () => {
|
|
|
820
866
|
);
|
|
821
867
|
});
|
|
822
868
|
|
|
869
|
+
test("closes an in-flight interview when ask_user_questions reports timed_out first", async (t) => {
|
|
870
|
+
const notifications: Array<{ message: string; type?: string }> = [];
|
|
871
|
+
let elicitationPromise: Promise<unknown> | undefined;
|
|
872
|
+
let elicitationSignalController: AbortController | undefined;
|
|
873
|
+
let customOpened = false;
|
|
874
|
+
const ui = {
|
|
875
|
+
custom: async (factory: any) => new Promise((resolve) => {
|
|
876
|
+
customOpened = true;
|
|
877
|
+
factory({ requestRender() {} } as any, {} as any, {} as any, resolve);
|
|
878
|
+
}),
|
|
879
|
+
notify: (message: string, type?: string) => {
|
|
880
|
+
notifications.push({ message, type });
|
|
881
|
+
},
|
|
882
|
+
};
|
|
883
|
+
t.after(() => {
|
|
884
|
+
elicitationSignalController?.abort();
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
const stream = streamViaClaudeCode(
|
|
888
|
+
{ id: "claude-sonnet-4-6" } as any,
|
|
889
|
+
{ messages: [{ role: "user", content: "Ask me." } as Message] },
|
|
890
|
+
{
|
|
891
|
+
extensionUIContext: ui as any,
|
|
892
|
+
_skipWorkflowMcpPreflightForTest: true,
|
|
893
|
+
async *_sdkQueryForTest(args: {
|
|
894
|
+
prompt: string | AsyncIterable<unknown>;
|
|
895
|
+
options?: Record<string, unknown>;
|
|
896
|
+
}) {
|
|
897
|
+
const onElicitation = args.options?.onElicitation as
|
|
898
|
+
| ((request: unknown, options: { signal: AbortSignal }) => Promise<unknown>)
|
|
899
|
+
| undefined;
|
|
900
|
+
assert.equal(typeof onElicitation, "function");
|
|
901
|
+
elicitationSignalController = new AbortController();
|
|
902
|
+
elicitationPromise = onElicitation!(askUserQuestionsRequest, {
|
|
903
|
+
signal: elicitationSignalController.signal,
|
|
904
|
+
});
|
|
905
|
+
assert.equal(customOpened, true, "interview overlay should be open before the tool result arrives");
|
|
906
|
+
|
|
907
|
+
// The adapter only assembles tool-call blocks after a
|
|
908
|
+
// message_start has created the partial-message builder. Real
|
|
909
|
+
// SDK streams always emit it before a tool_use content block;
|
|
910
|
+
// without it the timed_out result is never matched to the
|
|
911
|
+
// in-flight ask_user_questions block, so the interview would
|
|
912
|
+
// never be closed.
|
|
913
|
+
yield {
|
|
914
|
+
type: "stream_event",
|
|
915
|
+
event: { type: "message_start", message: { model: "claude-sonnet-4-6" } },
|
|
916
|
+
parent_tool_use_id: null,
|
|
917
|
+
uuid: "partial-1",
|
|
918
|
+
session_id: "session-1",
|
|
919
|
+
};
|
|
920
|
+
yield {
|
|
921
|
+
type: "stream_event",
|
|
922
|
+
event: {
|
|
923
|
+
type: "content_block_start",
|
|
924
|
+
index: 0,
|
|
925
|
+
content_block: {
|
|
926
|
+
type: "tool_use",
|
|
927
|
+
id: "ask-1",
|
|
928
|
+
name: "mcp__gsd-workflow__ask_user_questions",
|
|
929
|
+
input: {},
|
|
930
|
+
},
|
|
931
|
+
},
|
|
932
|
+
parent_tool_use_id: null,
|
|
933
|
+
uuid: "partial-1",
|
|
934
|
+
session_id: "session-1",
|
|
935
|
+
};
|
|
936
|
+
yield {
|
|
937
|
+
type: "user",
|
|
938
|
+
uuid: "user-result-1",
|
|
939
|
+
session_id: "session-1",
|
|
940
|
+
parent_tool_use_id: "ask-1",
|
|
941
|
+
message: {
|
|
942
|
+
role: "user",
|
|
943
|
+
content: [{
|
|
944
|
+
type: "mcp_tool_result",
|
|
945
|
+
tool_use_id: "ask-1",
|
|
946
|
+
content: [{ type: "text", text: "timed out" }],
|
|
947
|
+
structuredContent: { timed_out: true },
|
|
948
|
+
is_error: false,
|
|
949
|
+
}],
|
|
950
|
+
},
|
|
951
|
+
};
|
|
952
|
+
yield {
|
|
953
|
+
type: "result",
|
|
954
|
+
subtype: "success",
|
|
955
|
+
uuid: "result-1",
|
|
956
|
+
session_id: "session-1",
|
|
957
|
+
duration_ms: 1,
|
|
958
|
+
duration_api_ms: 1,
|
|
959
|
+
is_error: false,
|
|
960
|
+
num_turns: 1,
|
|
961
|
+
result: "done",
|
|
962
|
+
stop_reason: "end_turn",
|
|
963
|
+
total_cost_usd: 0,
|
|
964
|
+
usage: {
|
|
965
|
+
input_tokens: 0,
|
|
966
|
+
output_tokens: 0,
|
|
967
|
+
cache_read_input_tokens: 0,
|
|
968
|
+
cache_creation_input_tokens: 0,
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
},
|
|
972
|
+
} as any,
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
await stream.result();
|
|
976
|
+
assert.ok(elicitationPromise);
|
|
977
|
+
let guard: ReturnType<typeof setTimeout> | undefined;
|
|
978
|
+
const elicitationResult = await Promise.race([
|
|
979
|
+
elicitationPromise,
|
|
980
|
+
new Promise<never>((_, reject) => {
|
|
981
|
+
guard = setTimeout(
|
|
982
|
+
() => reject(new Error("timed out waiting for in-flight interview to close")),
|
|
983
|
+
250,
|
|
984
|
+
);
|
|
985
|
+
}),
|
|
986
|
+
]).finally(() => {
|
|
987
|
+
if (guard) clearTimeout(guard);
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
assert.deepEqual(elicitationResult, { action: "decline" });
|
|
991
|
+
assert.deepEqual(notifications, [{
|
|
992
|
+
message: "Question expired before you answered - the form was closed and no answers were sent.",
|
|
993
|
+
type: "warning",
|
|
994
|
+
}]);
|
|
995
|
+
});
|
|
996
|
+
|
|
823
997
|
test("serverToolUseToToolCallLike preserves object input for extension tool_result routing", () => {
|
|
824
998
|
const toolCall = serverToolUseToToolCallLike({
|
|
825
999
|
id: "srv-1",
|
|
@@ -2595,44 +2769,6 @@ describe("stream-adapter — workflow MCP readiness", () => {
|
|
|
2595
2769
|
});
|
|
2596
2770
|
|
|
2597
2771
|
describe("stream-adapter — MCP elicitation bridge", () => {
|
|
2598
|
-
const askUserQuestionsRequest = {
|
|
2599
|
-
serverName: "gsd-workflow",
|
|
2600
|
-
message: "Please answer the following question(s).",
|
|
2601
|
-
mode: "form" as const,
|
|
2602
|
-
requestedSchema: {
|
|
2603
|
-
type: "object" as const,
|
|
2604
|
-
properties: {
|
|
2605
|
-
storage_scope: {
|
|
2606
|
-
type: "string",
|
|
2607
|
-
title: "Storage",
|
|
2608
|
-
description: "Does this app need to sync across devices?",
|
|
2609
|
-
oneOf: [
|
|
2610
|
-
{ const: "Local-only (Recommended)", title: "Local-only (Recommended)" },
|
|
2611
|
-
{ const: "Cloud-synced", title: "Cloud-synced" },
|
|
2612
|
-
{ const: "None of the above", title: "None of the above" },
|
|
2613
|
-
],
|
|
2614
|
-
},
|
|
2615
|
-
storage_scope__note: {
|
|
2616
|
-
type: "string",
|
|
2617
|
-
title: "Storage Note",
|
|
2618
|
-
description: "Optional note for None of the above.",
|
|
2619
|
-
},
|
|
2620
|
-
platform: {
|
|
2621
|
-
type: "array",
|
|
2622
|
-
title: "Platform",
|
|
2623
|
-
description: "Where should it run?",
|
|
2624
|
-
items: {
|
|
2625
|
-
anyOf: [
|
|
2626
|
-
{ const: "Web", title: "Web" },
|
|
2627
|
-
{ const: "Desktop", title: "Desktop" },
|
|
2628
|
-
{ const: "Mobile", title: "Mobile" },
|
|
2629
|
-
],
|
|
2630
|
-
},
|
|
2631
|
-
},
|
|
2632
|
-
},
|
|
2633
|
-
},
|
|
2634
|
-
};
|
|
2635
|
-
|
|
2636
2772
|
test("parseAskUserQuestionsElicitation rebuilds interview questions from the MCP schema", () => {
|
|
2637
2773
|
const questions = parseAskUserQuestionsElicitation(askUserQuestionsRequest);
|
|
2638
2774
|
assert.deepEqual(questions, [
|
|
@@ -3041,6 +3177,43 @@ describe("stream-adapter — MCP elicitation bridge", () => {
|
|
|
3041
3177
|
assert.deepEqual(result, { action: "decline" });
|
|
3042
3178
|
});
|
|
3043
3179
|
|
|
3180
|
+
test("expires the interview overlay before the MCP elicitation response is dropped", async (t) => {
|
|
3181
|
+
mock.timers.enable({ apis: ["setTimeout"] });
|
|
3182
|
+
t.after(() => {
|
|
3183
|
+
mock.timers.reset();
|
|
3184
|
+
});
|
|
3185
|
+
|
|
3186
|
+
const notifications: Array<{ message: string; type?: string }> = [];
|
|
3187
|
+
let customOpened = false;
|
|
3188
|
+
const handler = createClaudeCodeElicitationHandler({
|
|
3189
|
+
custom: async (factory: any) => new Promise((resolve) => {
|
|
3190
|
+
customOpened = true;
|
|
3191
|
+
factory({ requestRender() {} } as any, {} as any, {} as any, resolve);
|
|
3192
|
+
}),
|
|
3193
|
+
notify: (message: string, type?: string) => {
|
|
3194
|
+
notifications.push({ message, type });
|
|
3195
|
+
},
|
|
3196
|
+
select: async () => {
|
|
3197
|
+
throw new Error("expired interview must not re-open dialog fallback");
|
|
3198
|
+
},
|
|
3199
|
+
} as any);
|
|
3200
|
+
assert.ok(handler);
|
|
3201
|
+
|
|
3202
|
+
const resultPromise = handler!(askUserQuestionsRequest, { signal: new AbortController().signal });
|
|
3203
|
+
await Promise.resolve();
|
|
3204
|
+
assert.equal(customOpened, true);
|
|
3205
|
+
|
|
3206
|
+
mock.timers.tick(CLAUDE_CODE_INTERVIEW_FORM_TIMEOUT_MS);
|
|
3207
|
+
const result = await resultPromise;
|
|
3208
|
+
|
|
3209
|
+
assert.deepEqual(result, { action: "decline" });
|
|
3210
|
+
assert.deepEqual(notifications, [{
|
|
3211
|
+
message: "Question expired before you answered - the form was closed and no answers were sent.",
|
|
3212
|
+
type: "warning",
|
|
3213
|
+
}]);
|
|
3214
|
+
assert.equal(isInteractiveElicitationInFlight(), false, "marker must clear after deadline expiry");
|
|
3215
|
+
});
|
|
3216
|
+
|
|
3044
3217
|
test("returns cancel (today's semantics) when the user genuinely dismisses", async () => {
|
|
3045
3218
|
const handler = createClaudeCodeElicitationHandler({
|
|
3046
3219
|
custom: async () => ({ endInterview: false, answers: {} }),
|
|
@@ -356,6 +356,19 @@ export function verifyExpectedArtifact(
|
|
|
356
356
|
try {
|
|
357
357
|
let taskIds: string[] | null = null;
|
|
358
358
|
let dbPrimary = false;
|
|
359
|
+
const planContent = readFileSync(absPath, "utf-8");
|
|
360
|
+
let parsedTaskIds: string[] | null = null;
|
|
361
|
+
const getParsedTaskIds = (): string[] => {
|
|
362
|
+
if (parsedTaskIds) return parsedTaskIds;
|
|
363
|
+
parsedTaskIds = parseLegacyPlan(planContent).tasks.map((t: { id: string }) => t.id);
|
|
364
|
+
return parsedTaskIds;
|
|
365
|
+
};
|
|
366
|
+
const tasksBlockMatch = planContent.match(/<tasks>([\s\S]*?)<\/tasks>/i);
|
|
367
|
+
const tasksBlock = tasksBlockMatch?.[1] ?? "";
|
|
368
|
+
const hasEmbeddedTaskEntries =
|
|
369
|
+
tasksBlock.length > 0 &&
|
|
370
|
+
(/^\s*- \[[xX ]\] \*\*T\d+/m.test(tasksBlock) ||
|
|
371
|
+
/^\s*#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(tasksBlock));
|
|
359
372
|
if (isDbAvailable()) {
|
|
360
373
|
const refreshed = refreshWorkflowDatabaseFromDisk();
|
|
361
374
|
if (refreshed) {
|
|
@@ -368,18 +381,17 @@ export function verifyExpectedArtifact(
|
|
|
368
381
|
}
|
|
369
382
|
|
|
370
383
|
if (!taskIds) {
|
|
371
|
-
const planContent = readFileSync(absPath, "utf-8");
|
|
372
384
|
const hasCheckboxTask = /^\s*- \[[xX ]\] \*\*T\d+/m.test(planContent);
|
|
373
385
|
const hasHeadingTask = /^\s*#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(planContent);
|
|
374
386
|
if (!hasCheckboxTask && !hasHeadingTask) {
|
|
375
387
|
logWarning("recovery", `verify-fail ${unitType} ${unitId}: plan has no task checkbox/heading (len=${planContent.length}) at ${absPath}`);
|
|
376
388
|
return false;
|
|
377
389
|
}
|
|
378
|
-
const
|
|
379
|
-
if (
|
|
390
|
+
const parsedIds = getParsedTaskIds();
|
|
391
|
+
if (parsedIds.length > 0) taskIds = parsedIds;
|
|
380
392
|
}
|
|
381
393
|
|
|
382
|
-
if (taskIds && taskIds.length > 0) {
|
|
394
|
+
if (taskIds && taskIds.length > 0 && !hasEmbeddedTaskEntries) {
|
|
383
395
|
const tasksDir = join(dirname(absPath), "tasks");
|
|
384
396
|
if (existsSync(tasksDir)) {
|
|
385
397
|
for (const tid of taskIds) {
|
|
@@ -79,13 +79,14 @@ export async function restorePreflightStashOrStop(
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* Run a milestone merge
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
82
|
+
* Run a milestone merge through Worktree Lifecycle's guarded merge option,
|
|
83
|
+
* which surrounds the inner merge with preflight stash + always-on postflight
|
|
84
|
+
* pop. The previous closeout code popped the stash only after a successful
|
|
85
|
+
* merge, which leaked `gsd-preflight-stash:M00x:*` entries whenever
|
|
86
|
+
* `mergeAndExit` threw — leaving the user's pre-merge working tree silently
|
|
87
|
+
* stashed away after a merge-conflict or other merge error. Lifecycle now
|
|
88
|
+
* restores the stash on every attempted merge path, then this adapter surfaces
|
|
89
|
+
* the merge or stash failure (in priority order) as the loop's stop reason.
|
|
89
90
|
*
|
|
90
91
|
* Returns a `break` action when auto-mode must stop, or `null` when the merge
|
|
91
92
|
* succeeded and the stash (if any) was restored cleanly.
|
|
@@ -97,66 +98,45 @@ export async function _runMilestoneMergeWithStashRestore(
|
|
|
97
98
|
): Promise<{ action: "break"; reason: string } | null> {
|
|
98
99
|
const { ctx, pi, s, deps } = ic;
|
|
99
100
|
|
|
100
|
-
const
|
|
101
|
-
|
|
101
|
+
const projectRoot = s.originalBasePath || s.basePath;
|
|
102
|
+
const mergeResult = deps.lifecycle.exitMilestone(
|
|
102
103
|
milestoneId,
|
|
103
|
-
|
|
104
|
+
{
|
|
105
|
+
merge: true,
|
|
106
|
+
guardedMerge: {
|
|
107
|
+
projectRoot,
|
|
108
|
+
preflightCleanRoot: deps.preflightCleanRoot,
|
|
109
|
+
postflightPopStash: deps.postflightPopStash,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
ctx.ui,
|
|
104
113
|
);
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
|
|
115
|
+
if (mergeResult.ok) {
|
|
116
|
+
await markMilestoneMergedAndRebuild(s);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (mergeResult.reason === "postflight-stash-restore-failed") {
|
|
121
|
+
await markMilestoneMergedAndRebuild(s);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (mergeResult.reason === "preflight-dirty-overlap" || mergeResult.reason === "preflight-unmerged-conflicts") {
|
|
125
|
+
const reason = mergeResult.reason === "preflight-unmerged-conflicts"
|
|
107
126
|
? `Pre-merge unresolved Git conflicts block milestone ${milestoneId}`
|
|
108
127
|
: `Pre-merge dirty working tree overlaps milestone ${milestoneId}`;
|
|
109
128
|
await deps.stopAuto(ctx, pi, reason, {
|
|
110
129
|
preserveCompletedMilestoneBranch: true,
|
|
111
130
|
preserveCloseoutTranscript: options.preserveCloseoutTranscript,
|
|
112
131
|
});
|
|
113
|
-
return {
|
|
114
|
-
action: "break",
|
|
115
|
-
reason: preflight.blockedReason === "unmerged-conflicts"
|
|
116
|
-
? "preflight-unmerged-conflicts"
|
|
117
|
-
: "preflight-dirty-overlap",
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
let mergeError: unknown = null;
|
|
122
|
-
const exitResult = deps.lifecycle.exitMilestone(
|
|
123
|
-
milestoneId,
|
|
124
|
-
{ merge: true },
|
|
125
|
-
ctx.ui,
|
|
126
|
-
);
|
|
127
|
-
if (exitResult.ok) {
|
|
128
|
-
s.milestoneMergedInPhases = true;
|
|
129
|
-
try {
|
|
130
|
-
const projectRoot = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
|
|
131
|
-
const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.js");
|
|
132
|
-
await rebuildMarkdownProjectionsFromDb(projectRoot);
|
|
133
|
-
} catch (err) {
|
|
134
|
-
logWarning(
|
|
135
|
-
"engine",
|
|
136
|
-
`markdown projection rebuild after milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
mergeError = exitResult.cause ?? new Error(`exit ${exitResult.reason}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Always attempt to restore the stashed working tree, even on merge error.
|
|
144
|
-
// postflightPopStash itself does not throw; failures surface via the
|
|
145
|
-
// PostflightResult.needsManualRecovery flag.
|
|
146
|
-
let stashResult: PostflightResult | null = null;
|
|
147
|
-
if (preflight.stashPushed) {
|
|
148
|
-
stashResult = deps.postflightPopStash(
|
|
149
|
-
s.originalBasePath || s.basePath,
|
|
150
|
-
milestoneId,
|
|
151
|
-
preflight.stashMarker,
|
|
152
|
-
ctx.ui.notify.bind(ctx.ui),
|
|
153
|
-
);
|
|
132
|
+
return { action: "break", reason: mergeResult.reason };
|
|
154
133
|
}
|
|
155
134
|
|
|
156
135
|
// Merge failure takes priority over stash recovery — the merge is the
|
|
157
136
|
// authoritative gate. If the stash also needed manual recovery, the user
|
|
158
137
|
// already saw the postflightPopStash notify above.
|
|
159
|
-
if (
|
|
138
|
+
if (mergeResult.reason === "merge-conflict") {
|
|
139
|
+
const mergeError = mergeResult.cause;
|
|
160
140
|
if (mergeError instanceof MergeConflictError) {
|
|
161
141
|
// A merge conflict is a recoverable human checkpoint, not an
|
|
162
142
|
// infrastructure failure — the user resolves the conflict and runs
|
|
@@ -173,6 +153,10 @@ export async function _runMilestoneMergeWithStashRestore(
|
|
|
173
153
|
});
|
|
174
154
|
return { action: "break", reason: "merge-conflict" };
|
|
175
155
|
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (mergeResult.reason === "merge-failed" || mergeResult.reason === "merge-conflict") {
|
|
159
|
+
const mergeError = mergeResult.cause;
|
|
176
160
|
logError("engine", "Milestone merge failed with non-conflict error", {
|
|
177
161
|
milestone: milestoneId,
|
|
178
162
|
error: String(mergeError),
|
|
@@ -190,12 +174,26 @@ export async function _runMilestoneMergeWithStashRestore(
|
|
|
190
174
|
return { action: "break", reason: "merge-failed" };
|
|
191
175
|
}
|
|
192
176
|
|
|
193
|
-
if (
|
|
194
|
-
return stopOnPostflightRecoveryNeeded(ic,
|
|
177
|
+
if (mergeResult.postflight) {
|
|
178
|
+
return stopOnPostflightRecoveryNeeded(ic, mergeResult.postflight, milestoneId);
|
|
195
179
|
}
|
|
196
180
|
return null;
|
|
197
181
|
}
|
|
198
182
|
|
|
183
|
+
async function markMilestoneMergedAndRebuild(s: AutoSession): Promise<void> {
|
|
184
|
+
s.milestoneMergedInPhases = true;
|
|
185
|
+
try {
|
|
186
|
+
const rebuildBasePath = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
|
|
187
|
+
const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.js");
|
|
188
|
+
await rebuildMarkdownProjectionsFromDb(rebuildBasePath);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
logWarning(
|
|
191
|
+
"engine",
|
|
192
|
+
`markdown projection rebuild after milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
199
197
|
export async function _runMilestoneMergeOnceWithStashRestore(
|
|
200
198
|
ic: IterationContext,
|
|
201
199
|
milestoneId: string,
|
|
@@ -373,6 +373,14 @@ export async function runFinalize(
|
|
|
373
373
|
files: leak.files.map((file) => ({ path: file.path, status: file.status })),
|
|
374
374
|
});
|
|
375
375
|
ctx.ui.notify(message, "error");
|
|
376
|
+
try {
|
|
377
|
+
deps.checkpointWorkflowDatabase?.();
|
|
378
|
+
} catch (err) {
|
|
379
|
+
debugLog("autoLoop", {
|
|
380
|
+
phase: "root-write-leak-checkpoint-failed",
|
|
381
|
+
error: err instanceof Error ? err.message : String(err),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
376
384
|
await deps.stopAuto(ctx, pi, "Root-write leak during isolated auto-mode", {
|
|
377
385
|
preserveCompletedMilestoneBranch: true,
|
|
378
386
|
});
|
|
@@ -731,7 +731,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
731
731
|
return new GitServiceImpl(basePath, gitConfig);
|
|
732
732
|
},
|
|
733
733
|
worktreeProjection: new WorktreeStateProjection(),
|
|
734
|
-
|
|
734
|
+
mergeMilestone: createMilestoneMergeTransaction(mergeMilestoneToMain),
|
|
735
735
|
});
|
|
736
736
|
}
|
|
737
737
|
|
|
@@ -26,7 +26,7 @@ import { debugLog } from "../debug-logger.js";
|
|
|
26
26
|
import { logWarning, logError } from "../workflow-logger.js";
|
|
27
27
|
import { resolveAutoSupervisorConfig } from "../preferences.js";
|
|
28
28
|
import { readUnitRuntimeRecord, type AutoUnitRuntimeRecord } from "../unit-runtime.js";
|
|
29
|
-
import { consumeAutoWakeup } from "./schedule-wakeup.js";
|
|
29
|
+
import { clearAutoWakeup, consumeAutoWakeup } from "./schedule-wakeup.js";
|
|
30
30
|
import { applyUnitSkillVisibility } from "../skill-scope.js";
|
|
31
31
|
|
|
32
32
|
const UNIT_FAILSAFE_BUFFER_MS = 30_000;
|
|
@@ -248,7 +248,7 @@ export async function runUnit(
|
|
|
248
248
|
((supervisor.idle_timeout_minutes ?? 10) * 60 * 1000) + UNIT_FAILSAFE_BUFFER_MS,
|
|
249
249
|
);
|
|
250
250
|
let unitTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
251
|
-
let result: UnitResult;
|
|
251
|
+
let result: UnitResult = { status: "cancelled" };
|
|
252
252
|
try {
|
|
253
253
|
const awaitAgentEndWithTimeout = (agentEndPromise: Promise<UnitResult>): Promise<UnitResult> => {
|
|
254
254
|
const timeoutResult = new Promise<UnitResult>((resolve) => {
|
|
@@ -360,6 +360,9 @@ export async function runUnit(
|
|
|
360
360
|
}
|
|
361
361
|
} finally {
|
|
362
362
|
if (unitTimeoutHandle) clearTimeout(unitTimeoutHandle);
|
|
363
|
+
if (result.status !== "completed") {
|
|
364
|
+
clearAutoWakeup(s.basePath, unitType, unitId);
|
|
365
|
+
}
|
|
363
366
|
ctx.ui.setWorkingMessage?.(undefined);
|
|
364
367
|
setVisibleSkills?.(undefined);
|
|
365
368
|
}
|