@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.6
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/CHANGELOG.md +209 -0
- package/dist/cli.js +3616 -3676
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/extensions/load-errors.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +10 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +9 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/index.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/model.d.ts +3 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +42 -6
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/session/session-paths.d.ts +21 -4
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/tools/xdev.d.ts +5 -3
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +1 -0
- package/dist/types/vibe/runtime.d.ts +23 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/exec/bash-executor.ts +68 -8
- package/src/extensibility/extensions/load-errors.ts +13 -0
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +41 -6
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +25 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/chat-transcript-builder.ts +10 -1
- package/src/modes/components/status-line/component.ts +57 -3
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +23 -122
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/components/usage-row.ts +17 -1
- package/src/modes/controllers/command-controller.ts +41 -1
- package/src/modes/controllers/event-controller.ts +7 -3
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +102 -60
- package/src/modes/noninteractive-dispose.test.ts +14 -1
- package/src/modes/print-mode.ts +81 -9
- package/src/modes/rpc/rpc-client.ts +76 -35
- package/src/modes/rpc/rpc-frame.ts +156 -0
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +11 -4
- package/src/modes/setup-wizard/index.ts +2 -0
- package/src/modes/setup-wizard/scenes/model.ts +134 -0
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +9 -3
- package/src/prompts/system/workflow-notice.md +89 -74
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +484 -188
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/session/session-manager.ts +34 -3
- package/src/session/session-paths.ts +38 -9
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/task/executor.ts +104 -33
- package/src/task/index.ts +46 -9
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/tools/xdev.ts +11 -4
- package/src/tools/yield.ts +4 -1
- package/src/utils/git.ts +237 -0
- package/src/{modes/components/status-line → utils}/token-rate.ts +6 -0
- package/src/vibe/__tests__/token-rate.test.ts +96 -0
- package/src/vibe/runtime.ts +50 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
- /package/dist/types/{modes/components/status-line → utils}/token-rate.d.ts +0 -0
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
type Agent,
|
|
9
9
|
AgentBusyError,
|
|
10
10
|
type AgentMessage,
|
|
11
|
-
type AgentToolResult,
|
|
12
11
|
EventLoopKeepalive,
|
|
13
12
|
ThinkingLevel,
|
|
14
13
|
} from "@oh-my-pi/pi-agent-core";
|
|
@@ -89,12 +88,7 @@ import {
|
|
|
89
88
|
MCP_CONNECTION_STATUS_EVENT_CHANNEL,
|
|
90
89
|
type McpConnectionStatusEvent,
|
|
91
90
|
} from "../mcp/startup-events";
|
|
92
|
-
import {
|
|
93
|
-
humanizePlanTitle,
|
|
94
|
-
type PlanApprovalDetails,
|
|
95
|
-
resolveApprovedPlan,
|
|
96
|
-
resolvePlanTitle,
|
|
97
|
-
} from "../plan-mode/approved-plan";
|
|
91
|
+
import { humanizePlanTitle, type PlanApprovalDetails, resolvePlanTitle } from "../plan-mode/approved-plan";
|
|
98
92
|
import { resolvePlanModelTransition } from "../plan-mode/model-transition";
|
|
99
93
|
import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
|
|
100
94
|
import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
|
|
@@ -129,7 +123,6 @@ import {
|
|
|
129
123
|
setActiveTodoDescriptionsProvider,
|
|
130
124
|
todoMatchesAnyDescription,
|
|
131
125
|
} from "../tools/todo";
|
|
132
|
-
import { ToolError } from "../tools/tool-errors";
|
|
133
126
|
import { vocalizer } from "../tts/vocalizer";
|
|
134
127
|
import { renderTreeList } from "../tui/tree-list";
|
|
135
128
|
import { copyToClipboard } from "../utils/clipboard";
|
|
@@ -138,7 +131,7 @@ import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
|
|
138
131
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
|
|
139
132
|
import { messageHasDisplayableThinking } from "../utils/thinking-display";
|
|
140
133
|
import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
|
|
141
|
-
import { VibeSessionRegistry } from "../vibe/runtime";
|
|
134
|
+
import { aggregateVibeWorkerTokensPerSecond, VibeSessionRegistry } from "../vibe/runtime";
|
|
142
135
|
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
143
136
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
144
137
|
import { ChatBlock, type ChatBlockHost } from "./components/chat-block";
|
|
@@ -208,6 +201,8 @@ import type {
|
|
|
208
201
|
} from "./types";
|
|
209
202
|
import { UiHelpers } from "./utils/ui-helpers";
|
|
210
203
|
|
|
204
|
+
const STILL_CLOSING_DELAY_MS = 3_000;
|
|
205
|
+
|
|
211
206
|
const HINT_SHIMMER_PALETTE: ShimmerPalette = {
|
|
212
207
|
low: "dim",
|
|
213
208
|
mid: "muted",
|
|
@@ -451,6 +446,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
451
446
|
vibeModeEnabled = false;
|
|
452
447
|
planModePlanFilePath: string | undefined = undefined;
|
|
453
448
|
loopModeEnabled = false;
|
|
449
|
+
loopModePaused = false;
|
|
454
450
|
loopPrompt: string | undefined = undefined;
|
|
455
451
|
loopLimit: LoopLimitRuntime | undefined = undefined;
|
|
456
452
|
#loopAutoSubmitTimer: NodeJS.Timeout | undefined;
|
|
@@ -696,6 +692,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
696
692
|
this.errorBannerContainer = new AnchoredLiveContainer();
|
|
697
693
|
this.modelCycleContainer = new AnchoredLiveContainer();
|
|
698
694
|
this.editor = new CustomEditor(getEditorTheme());
|
|
695
|
+
this.ui.enableScopedInputRender(this.editor);
|
|
699
696
|
this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
|
|
700
697
|
this.editor.setImeSafeCursorLayout(settings.get("tui.imeSafeCursor"));
|
|
701
698
|
this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
|
|
@@ -726,6 +723,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
726
723
|
this.editorContainer.addChild(this.editor);
|
|
727
724
|
this.statusLine = new StatusLineComponent(session);
|
|
728
725
|
this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
726
|
+
// Vibe worker tok/s aggregator — keeps the status-line render layer off
|
|
727
|
+
// the heavy vibe/task dependency graph. The director is often idle while
|
|
728
|
+
// workers stream, so without this the tok/s badge would show a stale
|
|
729
|
+
// value while parallel work is actively generating tokens.
|
|
730
|
+
this.statusLine.setVibeWorkerTokenRateProvider(() =>
|
|
731
|
+
aggregateVibeWorkerTokensPerSecond(this.session.getAgentId() ?? MAIN_AGENT_ID),
|
|
732
|
+
);
|
|
729
733
|
// Lazy provider — the top border rebuild coalesces to at most one
|
|
730
734
|
// invocation per painted frame instead of firing on every session event
|
|
731
735
|
// (#4145). The TUI throttles renders at ~30fps, so a long-running eval
|
|
@@ -1349,6 +1353,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1349
1353
|
this.disableLoopMode("Loop limit reached. Loop mode disabled.");
|
|
1350
1354
|
return;
|
|
1351
1355
|
}
|
|
1356
|
+
this.#syncLoopModeStatus();
|
|
1352
1357
|
|
|
1353
1358
|
if (action === "compact") {
|
|
1354
1359
|
await this.handleCompactCommand();
|
|
@@ -1358,19 +1363,36 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1358
1363
|
this.#submitLoopPromptWhenReady(prompt);
|
|
1359
1364
|
}
|
|
1360
1365
|
|
|
1366
|
+
#syncLoopModeStatus(): void {
|
|
1367
|
+
const state: "waiting" | "running" | "paused" = this.loopModePaused
|
|
1368
|
+
? "paused"
|
|
1369
|
+
: this.loopPrompt
|
|
1370
|
+
? "running"
|
|
1371
|
+
: "waiting";
|
|
1372
|
+
this.statusLine.setLoopModeStatus(this.loopModeEnabled ? { state, limit: this.loopLimit } : undefined);
|
|
1373
|
+
this.ui.requestRender();
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1361
1376
|
disableLoopMode(message = "Loop mode disabled."): void {
|
|
1362
1377
|
const wasEnabled = this.loopModeEnabled;
|
|
1363
1378
|
this.loopModeEnabled = false;
|
|
1379
|
+
this.loopModePaused = false;
|
|
1364
1380
|
this.loopPrompt = undefined;
|
|
1365
1381
|
this.loopLimit = undefined;
|
|
1366
1382
|
this.#cancelLoopAutoSubmit();
|
|
1367
|
-
this
|
|
1368
|
-
this.ui.requestRender();
|
|
1383
|
+
this.#syncLoopModeStatus();
|
|
1369
1384
|
if (wasEnabled) {
|
|
1370
1385
|
this.showStatus(message);
|
|
1371
1386
|
}
|
|
1372
1387
|
}
|
|
1373
1388
|
|
|
1389
|
+
setLoopPrompt(prompt: string): void {
|
|
1390
|
+
if (!this.loopModeEnabled) return;
|
|
1391
|
+
this.loopPrompt = prompt;
|
|
1392
|
+
this.loopModePaused = false;
|
|
1393
|
+
this.#syncLoopModeStatus();
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1374
1396
|
/**
|
|
1375
1397
|
* Pause the loop without exiting it: drops the captured prompt and any
|
|
1376
1398
|
* pending auto-resubmit. Loop mode stays enabled — the next prompt the
|
|
@@ -1378,7 +1400,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1378
1400
|
*/
|
|
1379
1401
|
pauseLoop(): void {
|
|
1380
1402
|
this.loopPrompt = undefined;
|
|
1403
|
+
this.loopModePaused = true;
|
|
1381
1404
|
this.#cancelLoopAutoSubmit();
|
|
1405
|
+
this.#syncLoopModeStatus();
|
|
1382
1406
|
}
|
|
1383
1407
|
|
|
1384
1408
|
async handleLoopCommand(args = ""): Promise<string | undefined> {
|
|
@@ -1392,10 +1416,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1392
1416
|
return undefined;
|
|
1393
1417
|
}
|
|
1394
1418
|
this.loopModeEnabled = true;
|
|
1419
|
+
this.loopModePaused = false;
|
|
1395
1420
|
this.loopPrompt = undefined;
|
|
1396
1421
|
this.loopLimit = createLoopLimitRuntime(parsed.limit);
|
|
1397
|
-
this
|
|
1398
|
-
this.ui.requestRender();
|
|
1422
|
+
this.#syncLoopModeStatus();
|
|
1399
1423
|
const limitSuffix = parsed.limit ? ` Limited to ${describeLoopLimit(parsed.limit)}.` : "";
|
|
1400
1424
|
const remainingSuffix = this.loopLimit ? ` ${describeLoopLimitRuntime(this.loopLimit)}.` : "";
|
|
1401
1425
|
const tail = parsed.prompt ? "Repeating it after each turn." : "Your next prompt will repeat after each turn.";
|
|
@@ -2301,7 +2325,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2301
2325
|
}
|
|
2302
2326
|
if (sessionContext.mode === "plan") {
|
|
2303
2327
|
const planFilePath = sessionContext.modeData?.planFilePath as string | undefined;
|
|
2304
|
-
await this.#enterPlanMode({ planFilePath });
|
|
2328
|
+
await this.#enterPlanMode({ planFilePath, preserveRestoredModel: true });
|
|
2305
2329
|
} else if (sessionContext.mode === "plan_paused") {
|
|
2306
2330
|
this.planModePaused = true;
|
|
2307
2331
|
this.#planModeHasEntered = true;
|
|
@@ -2309,7 +2333,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2309
2333
|
}
|
|
2310
2334
|
}
|
|
2311
2335
|
|
|
2312
|
-
async #enterPlanMode(options?: {
|
|
2336
|
+
async #enterPlanMode(options?: {
|
|
2337
|
+
planFilePath?: string;
|
|
2338
|
+
workflow?: "parallel" | "iterative";
|
|
2339
|
+
preserveRestoredModel?: boolean;
|
|
2340
|
+
}): Promise<void> {
|
|
2313
2341
|
if (this.planModeEnabled) {
|
|
2314
2342
|
return;
|
|
2315
2343
|
}
|
|
@@ -2356,44 +2384,22 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2356
2384
|
workflow: options?.workflow ?? "parallel",
|
|
2357
2385
|
reentry: this.#planModeHasEntered,
|
|
2358
2386
|
});
|
|
2359
|
-
this.session.setPlanProposalHandler?.(title => this
|
|
2387
|
+
this.session.setPlanProposalHandler?.(title => this.session.preparePlanForReview(title));
|
|
2360
2388
|
if (this.session.isStreaming) {
|
|
2361
2389
|
await this.session.sendPlanModeContext({ deliverAs: "steer" });
|
|
2362
2390
|
}
|
|
2363
2391
|
this.#planModeHasEntered = true;
|
|
2364
|
-
|
|
2392
|
+
// Session loading already restored the model recorded in the journal.
|
|
2393
|
+
// Reapplying today's plan role here would replace a CLI/session-specific
|
|
2394
|
+
// selection with current config during --resume or an in-process switch.
|
|
2395
|
+
if (!options?.preserveRestoredModel) {
|
|
2396
|
+
await this.#applyPlanModeModel();
|
|
2397
|
+
}
|
|
2365
2398
|
this.#updatePlanModeStatus();
|
|
2366
2399
|
this.sessionManager.appendModeChange("plan", { planFilePath });
|
|
2367
2400
|
this.showStatus(`Plan mode enabled. Plan file: ${planFilePath}`);
|
|
2368
2401
|
}
|
|
2369
2402
|
|
|
2370
|
-
/** Plan-proposal handler registered while plan mode is active. The agent
|
|
2371
|
-
* submits the finalized plan by writing the chosen `<slug>`/title to
|
|
2372
|
-
* `xd://propose`; this handler validates the plan file exists, normalizes
|
|
2373
|
-
* the title, and shapes the payload that `event-controller` forwards to
|
|
2374
|
-
* `handlePlanApproval`. */
|
|
2375
|
-
async #handlePlanProposal(title: string): Promise<AgentToolResult<unknown>> {
|
|
2376
|
-
const state = this.session.getPlanModeState?.();
|
|
2377
|
-
if (!state?.enabled) {
|
|
2378
|
-
throw new ToolError("Plan mode is not active.");
|
|
2379
|
-
}
|
|
2380
|
-
const { planFilePath, title: resolvedTitle } = await resolveApprovedPlan({
|
|
2381
|
-
suppliedTitle: title,
|
|
2382
|
-
statePlanFilePath: state.planFilePath,
|
|
2383
|
-
readPlan: url => this.#readPlanFile(url),
|
|
2384
|
-
listPlanFiles: () => this.#listLocalPlanFiles(),
|
|
2385
|
-
});
|
|
2386
|
-
const details: PlanApprovalDetails = {
|
|
2387
|
-
planFilePath,
|
|
2388
|
-
title: resolvedTitle,
|
|
2389
|
-
planExists: true,
|
|
2390
|
-
};
|
|
2391
|
-
return {
|
|
2392
|
-
content: [{ type: "text" as const, text: "Plan ready for approval." }],
|
|
2393
|
-
details,
|
|
2394
|
-
};
|
|
2395
|
-
}
|
|
2396
|
-
|
|
2397
2403
|
async #restorePlanPreviousModel(prev: { model: Model; thinkingLevel?: ConfiguredThinkingLevel }): Promise<void> {
|
|
2398
2404
|
if (modelsAreEqual(this.session.model, prev.model)) {
|
|
2399
2405
|
// Same model — only thinking level may differ. Avoid setModelTemporary()
|
|
@@ -2434,26 +2440,54 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2434
2440
|
}
|
|
2435
2441
|
|
|
2436
2442
|
const planModeState = this.session.getPlanModeState();
|
|
2443
|
+
const planModeTools = this.session.getEnabledToolNames();
|
|
2444
|
+
const planModeMountedTools = this.session.getMountedXdevToolNames();
|
|
2445
|
+
const planModeModelState = this.session.model
|
|
2446
|
+
? { model: this.session.model, thinkingLevel: this.session.configuredThinkingLevel() }
|
|
2447
|
+
: undefined;
|
|
2437
2448
|
this.session.setPlanModeState(undefined);
|
|
2438
2449
|
try {
|
|
2439
2450
|
if (this.#planModePreviousTools !== undefined) {
|
|
2440
2451
|
await this.session.setActiveToolsByName(this.#planModePreviousTools);
|
|
2441
2452
|
}
|
|
2442
|
-
if (this.#planModePreviousModelState) {
|
|
2443
|
-
|
|
2444
|
-
await this.#restorePlanPreviousModel(this.#planModePreviousModelState);
|
|
2445
|
-
}
|
|
2446
|
-
// If #applyPlanModeModel queued a deferred switch to the plan-role model
|
|
2447
|
-
// (because the session was streaming on entry), drop it now: we are
|
|
2448
|
-
// leaving plan mode, so flushing it on the next agent_end would land the
|
|
2449
|
-
// session on the plan-role model after the user has exited plan mode
|
|
2450
|
-
// (issue #816). This runs even when deferModelRestore is set
|
|
2451
|
-
// (compact-approval path): otherwise the stale plan switch survives and
|
|
2452
|
-
// flushPendingModelSwitch() later clobbers the restored/execution model.
|
|
2453
|
-
this.#clearPendingPlanModelSwitch();
|
|
2453
|
+
if (this.#planModePreviousModelState && !options?.deferModelRestore) {
|
|
2454
|
+
await this.#restorePlanPreviousModel(this.#planModePreviousModelState);
|
|
2454
2455
|
}
|
|
2456
|
+
// If #applyPlanModeModel queued a deferred switch to the plan-role model
|
|
2457
|
+
// (because the session was streaming on entry), drop it now: we are
|
|
2458
|
+
// leaving plan mode, so flushing it on the next agent_end would land the
|
|
2459
|
+
// session on the plan-role model after the user has exited plan mode
|
|
2460
|
+
// (issue #816). This runs even when deferModelRestore is set
|
|
2461
|
+
// (compact-approval path): otherwise the stale plan switch survives and
|
|
2462
|
+
// flushPendingModelSwitch() later clobbers the restored/execution model.
|
|
2463
|
+
if (this.#planModePreviousModelState) this.#clearPendingPlanModelSwitch();
|
|
2455
2464
|
} catch (error) {
|
|
2456
2465
|
this.session.setPlanModeState(planModeState);
|
|
2466
|
+
if (
|
|
2467
|
+
planModeModelState &&
|
|
2468
|
+
(!modelsAreEqual(this.session.model, planModeModelState.model) ||
|
|
2469
|
+
this.session.configuredThinkingLevel() !== planModeModelState.thinkingLevel)
|
|
2470
|
+
) {
|
|
2471
|
+
try {
|
|
2472
|
+
await this.#restorePlanPreviousModel(planModeModelState);
|
|
2473
|
+
} catch (rollbackError) {
|
|
2474
|
+
logger.warn("Failed to restore plan model after plan exit failure", { error: String(rollbackError) });
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
const enabledTools = this.session.getEnabledToolNames();
|
|
2478
|
+
const mountedTools = this.session.getMountedXdevToolNames();
|
|
2479
|
+
if (
|
|
2480
|
+
enabledTools.length !== planModeTools.length ||
|
|
2481
|
+
enabledTools.some((name, index) => name !== planModeTools[index]) ||
|
|
2482
|
+
mountedTools.length !== planModeMountedTools.length ||
|
|
2483
|
+
mountedTools.some((name, index) => name !== planModeMountedTools[index])
|
|
2484
|
+
) {
|
|
2485
|
+
try {
|
|
2486
|
+
await this.session.setActiveToolPresentation(planModeTools, planModeMountedTools);
|
|
2487
|
+
} catch (rollbackError) {
|
|
2488
|
+
logger.warn("Failed to restore plan tools after plan exit failure", { error: String(rollbackError) });
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2457
2491
|
throw error;
|
|
2458
2492
|
}
|
|
2459
2493
|
this.session.setPlanProposalHandler?.(null);
|
|
@@ -3746,10 +3780,17 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3746
3780
|
// first runs the work, the other awaits the same settled promise.
|
|
3747
3781
|
// The teardown is registered lazily in `init()` — a `/exit` reached
|
|
3748
3782
|
// before `init()` completed falls back to a direct dispose.
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
}
|
|
3752
|
-
|
|
3783
|
+
const stillClosingTimer = setTimeout(() => {
|
|
3784
|
+
this.showStatus("Still closing… (flushing memory backend / network)");
|
|
3785
|
+
}, STILL_CLOSING_DELAY_MS);
|
|
3786
|
+
try {
|
|
3787
|
+
if (this.#signalTeardown) {
|
|
3788
|
+
await this.#signalTeardown();
|
|
3789
|
+
} else {
|
|
3790
|
+
await this.session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
|
|
3791
|
+
}
|
|
3792
|
+
} finally {
|
|
3793
|
+
clearTimeout(stillClosingTimer);
|
|
3753
3794
|
}
|
|
3754
3795
|
|
|
3755
3796
|
// Do not force a final render during teardown: disposed session/UI state can
|
|
@@ -3794,6 +3835,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3794
3835
|
const nextEditor = factory
|
|
3795
3836
|
? factory(this.ui, getEditorTheme(), this.keybindings)
|
|
3796
3837
|
: new CustomEditor(getEditorTheme());
|
|
3838
|
+
if (!factory) this.ui.enableScopedInputRender(nextEditor);
|
|
3797
3839
|
|
|
3798
3840
|
nextEditor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
|
|
3799
3841
|
nextEditor.setImeSafeCursorLayout(this.settings.get("tui.imeSafeCursor"));
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { describe, expect, it, spyOn } from "bun:test";
|
|
9
9
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import type { AgentSession } from "../session/agent-session";
|
|
11
|
+
import * as telemetryExport from "../telemetry-export";
|
|
11
12
|
import { runPrintMode } from "./print-mode";
|
|
12
13
|
|
|
13
14
|
/** Stand-in for `process.exit`: it terminates, so nothing after it should run. */
|
|
@@ -34,12 +35,23 @@ describe("print-mode error exit disposes the session before exit", () => {
|
|
|
34
35
|
const session = {
|
|
35
36
|
extensionRunner: undefined,
|
|
36
37
|
subscribe: () => {},
|
|
38
|
+
settings: { get: () => false },
|
|
39
|
+
sessionManager: { buildSessionContext: () => ({ messages: [] }), getEntries: () => [] },
|
|
37
40
|
state: { messages: [errorMsg] },
|
|
41
|
+
getLastAssistantMessage: () => errorMsg,
|
|
42
|
+
prepareForHeadlessAdvisorDrain: () => {},
|
|
43
|
+
waitForAdvisorCatchup: async () => {
|
|
44
|
+
order.push("catchup");
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
38
47
|
dispose: async () => {
|
|
39
48
|
order.push("dispose");
|
|
40
49
|
},
|
|
41
50
|
} as unknown as AgentSession;
|
|
42
51
|
|
|
52
|
+
const flushSpy = spyOn(telemetryExport, "flushTelemetryExport").mockImplementation(async () => {
|
|
53
|
+
order.push("flush");
|
|
54
|
+
});
|
|
43
55
|
const exitSpy = spyOn(process, "exit").mockImplementation(((code: number) => {
|
|
44
56
|
order.push("exit");
|
|
45
57
|
throw new ProcessExit(code);
|
|
@@ -53,8 +65,9 @@ describe("print-mode error exit disposes the session before exit", () => {
|
|
|
53
65
|
} finally {
|
|
54
66
|
exitSpy.mockRestore();
|
|
55
67
|
stderrSpy.mockRestore();
|
|
68
|
+
flushSpy.mockRestore();
|
|
56
69
|
}
|
|
57
70
|
|
|
58
|
-
expect(order).toEqual(["dispose", "exit"]);
|
|
71
|
+
expect(order).toEqual(["catchup", "flush", "dispose", "exit"]);
|
|
59
72
|
});
|
|
60
73
|
});
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* - `omp --mode json "prompt"` - JSON event stream
|
|
7
7
|
*/
|
|
8
8
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
9
|
-
import type {
|
|
9
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { resolvePlanModelTransition } from "../plan-mode/model-transition";
|
|
11
12
|
import { type AgentSession, type AgentSessionEvent, SHUTDOWN_CONSOLIDATE_BUDGET_MS } from "../session/agent-session";
|
|
12
13
|
import { isSilentAbort } from "../session/messages";
|
|
13
14
|
import { flushTelemetryExport } from "../telemetry-export";
|
|
15
|
+
import { PROPOSE_DEVICE_NAME, writeDeviceDispatch } from "../tools/resolve";
|
|
14
16
|
import { initializeExtensions } from "./runtime-init";
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -29,6 +31,11 @@ export interface PrintModeOptions {
|
|
|
29
31
|
printThoughts?: boolean;
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/** Matches the longest built-in provider request deadline while bounding tool-loop stalls. */
|
|
35
|
+
export const PRINT_MODE_ADVISOR_DRAIN_TIMEOUT_MS = 10 * 60_000;
|
|
36
|
+
/** Error exits cannot hold automation for the full normal drain budget. */
|
|
37
|
+
export const PRINT_MODE_ERROR_ADVISOR_DRAIN_TIMEOUT_MS = 30_000;
|
|
38
|
+
|
|
32
39
|
/** Drop the provider-opaque replay payload (e.g. encrypted reasoning items) before printing. */
|
|
33
40
|
function stripProviderPayload<T extends AgentMessage>(message: T): T {
|
|
34
41
|
if (!("providerPayload" in message) || message.providerPayload === undefined) return message;
|
|
@@ -103,8 +110,66 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
103
110
|
},
|
|
104
111
|
});
|
|
105
112
|
|
|
113
|
+
// InteractiveMode applies the same startup default during TUI initialization.
|
|
114
|
+
// Print mode has no TUI bootstrap, so arm the shared session directly before
|
|
115
|
+
// the first prompt; persisting the mode_change also lets a later interactive
|
|
116
|
+
// attachment restore and review the generated plan.
|
|
117
|
+
let abortAfterPlanProposal = false;
|
|
118
|
+
const planDefaultArmed =
|
|
119
|
+
session.settings.get("plan.defaultOnStartup") &&
|
|
120
|
+
session.settings.get("plan.enabled") &&
|
|
121
|
+
session.sessionManager.buildSessionContext().messages.length === 0 &&
|
|
122
|
+
!session.sessionManager.getEntries().some(entry => entry.type === "mode_change");
|
|
123
|
+
if (planDefaultArmed) {
|
|
124
|
+
const planFilePath = session.getPlanReferencePath() || "local://PLAN.md";
|
|
125
|
+
const previousTools = session.getEnabledToolNames();
|
|
126
|
+
const planTools = session.hasBuiltInTool("write") ? [...new Set([...previousTools, "write"])] : previousTools;
|
|
127
|
+
await session.setActiveToolsByName(planTools);
|
|
128
|
+
session.setPlanModeState({
|
|
129
|
+
enabled: true,
|
|
130
|
+
planFilePath,
|
|
131
|
+
workflow: "parallel",
|
|
132
|
+
});
|
|
133
|
+
session.sessionManager.appendModeChange("plan", { planFilePath });
|
|
134
|
+
abortAfterPlanProposal = true;
|
|
135
|
+
session.setPlanProposalHandler(async title => {
|
|
136
|
+
const result = await session.preparePlanForReview(title);
|
|
137
|
+
const details = result.details;
|
|
138
|
+
if (details) {
|
|
139
|
+
const state = session.getPlanModeState();
|
|
140
|
+
if (state?.enabled) {
|
|
141
|
+
session.setPlanModeState({ ...state, planFilePath: details.planFilePath });
|
|
142
|
+
}
|
|
143
|
+
session.sessionManager.appendModeChange("plan", { planFilePath: details.planFilePath });
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const resolved = session.resolveRoleModelWithThinking("plan");
|
|
149
|
+
const transition = resolvePlanModelTransition(session.model, resolved, false);
|
|
150
|
+
if (transition.kind === "thinking") {
|
|
151
|
+
session.setThinkingLevel(transition.thinkingLevel);
|
|
152
|
+
} else if (transition.kind === "apply") {
|
|
153
|
+
try {
|
|
154
|
+
await session.setModelTemporary(transition.model, transition.thinkingLevel);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
logger.warn("Failed to switch to plan model for print mode", { error: String(error) });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
106
161
|
// Always subscribe to enable session persistence via _handleAgentEvent
|
|
107
162
|
session.subscribe(event => {
|
|
163
|
+
if (abortAfterPlanProposal && event.type === "tool_execution_end" && !event.isError) {
|
|
164
|
+
const dispatch = writeDeviceDispatch(event.toolName, event.result);
|
|
165
|
+
if (dispatch?.tool === PROPOSE_DEVICE_NAME && dispatch.mode === "execute") {
|
|
166
|
+
abortAfterPlanProposal = false;
|
|
167
|
+
session.markPlanInternalAbortPending();
|
|
168
|
+
void session.abort().finally(() => {
|
|
169
|
+
session.clearPlanInternalAbortPending();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
108
173
|
// In JSON mode, output all events
|
|
109
174
|
if (mode === "json") {
|
|
110
175
|
process.stdout.write(`${JSON.stringify(printableEvent(event))}\n`);
|
|
@@ -130,14 +195,19 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
130
195
|
await logger.time("print:prompt:next", () => session.prompt(message));
|
|
131
196
|
}
|
|
132
197
|
|
|
198
|
+
// From this point onward a late blocker must be recorded without starting a
|
|
199
|
+
// primary turn whose response print mode would never emit.
|
|
200
|
+
session.prepareForHeadlessAdvisorDrain();
|
|
201
|
+
|
|
133
202
|
// In text mode, output final response
|
|
134
203
|
if (mode === "text") {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
204
|
+
// Read via the session accessor, not the raw state tail: a classifier
|
|
205
|
+
// refusal is pruned from active context at settle, and an aborted turn
|
|
206
|
+
// can trail synthetic tool results — both would hide the terminal
|
|
207
|
+
// assistant message (and its error) from a last-element read.
|
|
208
|
+
const assistantMsg = session.getLastAssistantMessage();
|
|
140
209
|
|
|
210
|
+
if (assistantMsg) {
|
|
141
211
|
// Check for error/aborted — skip silent-abort (plan-mode compaction transition)
|
|
142
212
|
if (
|
|
143
213
|
(assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") &&
|
|
@@ -151,6 +221,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
151
221
|
// `dispose()` (releaseTabsForOwner) actually runs — otherwise an
|
|
152
222
|
// OMP-owned Chromium survives this exit (issue #5643). `dispose()`
|
|
153
223
|
// is idempotent, so the unreachable call below is a harmless no-op.
|
|
224
|
+
await session.waitForAdvisorCatchup(PRINT_MODE_ERROR_ADVISOR_DRAIN_TIMEOUT_MS);
|
|
154
225
|
await flushTelemetryExport();
|
|
155
226
|
await session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
|
|
156
227
|
const flushed = process.stderr.write(`${errorLine}\n`);
|
|
@@ -180,14 +251,15 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
180
251
|
}
|
|
181
252
|
}
|
|
182
253
|
|
|
183
|
-
|
|
184
|
-
|
|
254
|
+
await session.waitForAdvisorCatchup(PRINT_MODE_ADVISOR_DRAIN_TIMEOUT_MS);
|
|
255
|
+
|
|
256
|
+
// Ensure stdout, including late JSON advisor events, is fully flushed before returning.
|
|
257
|
+
// This prevents race conditions where the process exits before all output is written.
|
|
185
258
|
await new Promise<void>((resolve, reject) => {
|
|
186
259
|
process.stdout.write("", err => {
|
|
187
260
|
if (err) reject(err);
|
|
188
261
|
else resolve();
|
|
189
262
|
});
|
|
190
263
|
});
|
|
191
|
-
|
|
192
264
|
await session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
|
|
193
265
|
}
|
|
@@ -206,6 +206,7 @@ function normalizeToolResult<TDetails>(result: RpcClientToolResult<TDetails>): A
|
|
|
206
206
|
|
|
207
207
|
export class RpcClient {
|
|
208
208
|
#process: ptree.ChildProcess | null = null;
|
|
209
|
+
#reaping: Promise<void> | null = null;
|
|
209
210
|
#eventListeners: RpcEventListener[] = [];
|
|
210
211
|
#sessionEventListeners: RpcSessionEventListener[] = [];
|
|
211
212
|
#subagentLifecycleListeners = new Set<RpcSubagentLifecycleListener>();
|
|
@@ -233,6 +234,7 @@ export class RpcClient {
|
|
|
233
234
|
* retry without leaking processes.
|
|
234
235
|
*/
|
|
235
236
|
async start(): Promise<void> {
|
|
237
|
+
await this.#reaping;
|
|
236
238
|
if (this.#process) {
|
|
237
239
|
throw new Error("Client already started");
|
|
238
240
|
}
|
|
@@ -268,7 +270,26 @@ export class RpcClient {
|
|
|
268
270
|
const { promise: readyPromise, resolve: readyResolve, reject: readyReject } = Promise.withResolvers<void>();
|
|
269
271
|
let readySettled = false;
|
|
270
272
|
|
|
271
|
-
|
|
273
|
+
const reapAfterOutputFailure = async (error: Error) => {
|
|
274
|
+
if (this.#process !== child) return;
|
|
275
|
+
|
|
276
|
+
this.#process = null;
|
|
277
|
+
this.#abortController.abort(error);
|
|
278
|
+
const pendingRequests = Array.from(this.#pendingRequests.values());
|
|
279
|
+
this.#pendingRequests.clear();
|
|
280
|
+
for (const pendingCall of this.#pendingHostToolCalls.values()) pendingCall.controller.abort(error);
|
|
281
|
+
this.#pendingHostToolCalls.clear();
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
child.kill();
|
|
285
|
+
} catch {
|
|
286
|
+
// The process may already have exited.
|
|
287
|
+
}
|
|
288
|
+
await this.#waitForExit(child);
|
|
289
|
+
for (const request of pendingRequests) request.reject(error);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Process lines in background, intercepting the ready signal.
|
|
272
293
|
const lines = readJsonl(child.stdout, this.#abortController.signal);
|
|
273
294
|
void (async () => {
|
|
274
295
|
for await (const line of lines) {
|
|
@@ -279,22 +300,38 @@ export class RpcClient {
|
|
|
279
300
|
}
|
|
280
301
|
this.#handleLine(line);
|
|
281
302
|
}
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
// rejecting here would snapshot a partial stderr tail and lose
|
|
286
|
-
// the actual startup error.
|
|
287
|
-
if (readySettled) return;
|
|
288
|
-
await child.exited.catch(() => {});
|
|
303
|
+
// A closed stdout is terminal even if the child remains alive. Startup
|
|
304
|
+
// failures are reaped by the readyPromise catch below; established
|
|
305
|
+
// workers are reaped here so pending requests cannot hang indefinitely.
|
|
289
306
|
if (!readySettled) {
|
|
290
307
|
readySettled = true;
|
|
291
|
-
readyReject(new Error(`Agent
|
|
308
|
+
readyReject(new Error(`Agent output stream ended before ready. Stderr: ${child.peekStderr()}`));
|
|
309
|
+
return;
|
|
292
310
|
}
|
|
293
|
-
|
|
311
|
+
const exitResult = await Promise.race([
|
|
312
|
+
child.exited.then(
|
|
313
|
+
exitCode => ({ exitCode }),
|
|
314
|
+
cause => ({ cause }),
|
|
315
|
+
),
|
|
316
|
+
Bun.sleep(100).then(() => null),
|
|
317
|
+
]);
|
|
318
|
+
const error =
|
|
319
|
+
exitResult === null
|
|
320
|
+
? new Error(`Agent output stream ended unexpectedly. Stderr: ${child.peekStderr()}`)
|
|
321
|
+
: "exitCode" in exitResult
|
|
322
|
+
? new Error(`Agent process exited with code ${exitResult.exitCode}. Stderr: ${child.peekStderr()}`)
|
|
323
|
+
: new Error(`Agent output stream ended. Stderr: ${child.peekStderr()}`, {
|
|
324
|
+
cause: exitResult.cause,
|
|
325
|
+
});
|
|
326
|
+
await reapAfterOutputFailure(error);
|
|
327
|
+
})().catch(async (cause: unknown) => {
|
|
328
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
294
329
|
if (!readySettled) {
|
|
295
330
|
readySettled = true;
|
|
296
|
-
readyReject(
|
|
331
|
+
readyReject(error);
|
|
332
|
+
return;
|
|
297
333
|
}
|
|
334
|
+
await reapAfterOutputFailure(new Error(`Agent output reader failed: ${error.message}`, { cause: error }));
|
|
298
335
|
});
|
|
299
336
|
|
|
300
337
|
// Also race against process exit (in case stdout closes before we read it)
|
|
@@ -325,20 +362,12 @@ export class RpcClient {
|
|
|
325
362
|
if (this.#customTools.length > 0) {
|
|
326
363
|
await this.setCustomTools(this.#customTools);
|
|
327
364
|
}
|
|
328
|
-
} catch (
|
|
329
|
-
// Startup failed after
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
} catch {
|
|
335
|
-
// best-effort cleanup
|
|
336
|
-
}
|
|
337
|
-
this.#abortController.abort();
|
|
338
|
-
if (this.#process === child) {
|
|
339
|
-
this.#process = null;
|
|
340
|
-
}
|
|
341
|
-
throw err;
|
|
365
|
+
} catch (cause) {
|
|
366
|
+
// Startup failed after spawning the child. Reap it before returning
|
|
367
|
+
// so a retry cannot inherit a live worker or its session lock.
|
|
368
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
369
|
+
await reapAfterOutputFailure(error);
|
|
370
|
+
throw cause;
|
|
342
371
|
} finally {
|
|
343
372
|
clearTimeout(readyTimeout);
|
|
344
373
|
}
|
|
@@ -347,28 +376,40 @@ export class RpcClient {
|
|
|
347
376
|
/**
|
|
348
377
|
* Stop the RPC agent process.
|
|
349
378
|
*/
|
|
350
|
-
stop() {
|
|
351
|
-
if (!this.#process) return;
|
|
379
|
+
stop(): Promise<void> {
|
|
380
|
+
if (!this.#process) return this.#reaping ?? Promise.resolve();
|
|
352
381
|
|
|
353
|
-
|
|
354
|
-
this.#
|
|
382
|
+
const error = new Error("Client stopped");
|
|
383
|
+
const child = this.#process;
|
|
384
|
+
child.kill();
|
|
385
|
+
this.#abortController.abort(error);
|
|
355
386
|
this.#process = null;
|
|
387
|
+
for (const request of this.#pendingRequests.values()) request.reject(error);
|
|
356
388
|
this.#pendingRequests.clear();
|
|
357
389
|
for (const pendingCall of this.#pendingHostToolCalls.values()) {
|
|
358
|
-
pendingCall.controller.abort();
|
|
390
|
+
pendingCall.controller.abort(error);
|
|
359
391
|
}
|
|
360
392
|
this.#pendingHostToolCalls.clear();
|
|
393
|
+
return this.#waitForExit(child);
|
|
361
394
|
}
|
|
362
395
|
|
|
363
396
|
/**
|
|
364
397
|
* Stop the RPC agent process and clean up resources.
|
|
365
398
|
*/
|
|
366
399
|
[Symbol.dispose](): void {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
400
|
+
void this.stop();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#waitForExit(child: ptree.ChildProcess): Promise<void> {
|
|
404
|
+
const reaping = child.exited.then(
|
|
405
|
+
() => {},
|
|
406
|
+
() => {},
|
|
407
|
+
);
|
|
408
|
+
this.#reaping = reaping;
|
|
409
|
+
void reaping.then(() => {
|
|
410
|
+
if (this.#reaping === reaping) this.#reaping = null;
|
|
411
|
+
});
|
|
412
|
+
return reaping;
|
|
372
413
|
}
|
|
373
414
|
|
|
374
415
|
/**
|