@asm-agent/coding-agent 0.8.8 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +2 -7
- package/dist/asm-agent-runtime/src/__pycache__/asm_cm_driver.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/__pycache__/phase76_address_head.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__init__.py +1 -1
- package/dist/asm-agent-runtime/src/rlm/__pycache__/__init__.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/bash.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/bash.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp_base.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/repl.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/repl.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/skill.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/bash.py +94 -31
- package/dist/asm-agent-runtime/src/rlm/repl.md +3 -3
- package/dist/asm-agent-runtime/src/rlm/repl.py +46 -45
- package/dist/asm-agent-runtime/test/__pycache__/test_bash.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/test/__pycache__/test_repl.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/test/test_bash.py +74 -19
- package/dist/asm-agent-runtime/test/test_repl.py +67 -3
- package/dist/asm-agent-runtime/uv.lock +446 -155
- package/dist/bundle/{anthropic-OFECORE7.js → anthropic-2VRH36LG.js} +2 -2
- package/dist/bundle/{azure-openai-responses-AOVTLR6N.js → azure-openai-responses-COYWHJPT.js} +2 -2
- package/dist/bundle/{bundled-modules-JQCJDEIY.js → bundled-modules-YKMFOFDW.js} +3 -3
- package/dist/bundle/{chunk-JC2TT4MZ.js → chunk-JBR32RCG.js} +4943 -1917
- package/dist/bundle/{chunk-M67RGWDQ.js → chunk-US4QGESM.js} +43 -44
- package/dist/bundle/{chunk-A65OM3TZ.js → chunk-V3BASDCN.js} +1 -1
- package/dist/bundle/{chunk-7TB7CGPV.js → chunk-YL37EJB6.js} +978 -1821
- package/dist/bundle/{cli-main-FNHDQGQD.js → cli-main-D7CFAHKG.js} +3 -3
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/{google-RVJL6RIQ.js → google-CSTXROSV.js} +1 -1
- package/dist/bundle/{google-vertex-EZTRRRH6.js → google-vertex-MEAR43KB.js} +1 -1
- package/dist/bundle/{main-IYXIIAJN.js → main-NKNJEUDP.js} +3 -3
- package/dist/bundle/{mistral-FSGKSMA7.js → mistral-VAF7JADB.js} +1 -1
- package/dist/bundle/{openai-codex-responses-VM5Y564D.js → openai-codex-responses-RWZ6P4I3.js} +2 -2
- package/dist/bundle/{openai-completions-X46FOQDN.js → openai-completions-CFB3BOTU.js} +3 -3
- package/dist/bundle/{openai-responses-SOQNEPBE.js → openai-responses-4OPGGCBI.js} +2 -2
- package/dist/config.d.ts +0 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -3
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-config.d.ts +1 -2
- package/dist/core/agent-session-config.d.ts.map +1 -1
- package/dist/core/agent-session-config.js +0 -3
- package/dist/core/agent-session-config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +0 -8
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +0 -2
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +0 -22
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +8 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +100 -22
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/kernel/bootstrap.d.ts.map +1 -1
- package/dist/core/kernel/bootstrap.js +1 -1
- package/dist/core/kernel/bootstrap.js.map +1 -1
- package/dist/core/kernel/repl-manager.d.ts +32 -8
- package/dist/core/kernel/repl-manager.d.ts.map +1 -1
- package/dist/core/kernel/repl-manager.js +401 -102
- package/dist/core/kernel/repl-manager.js.map +1 -1
- package/dist/core/kernel/shared.d.ts +11 -7
- package/dist/core/kernel/shared.d.ts.map +1 -1
- package/dist/core/kernel/shared.js +2 -5
- package/dist/core/kernel/shared.js.map +1 -1
- package/dist/core/mcp/mcp-manager.d.ts +4 -2
- package/dist/core/mcp/mcp-manager.d.ts.map +1 -1
- package/dist/core/mcp/mcp-manager.js +9 -7
- package/dist/core/mcp/mcp-manager.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +1 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/prime-inference-auth.d.ts +0 -9
- package/dist/core/prime-inference-auth.d.ts.map +1 -1
- package/dist/core/prime-inference-auth.js +0 -50
- package/dist/core/prime-inference-auth.js.map +1 -1
- package/dist/core/provider-display-names.d.ts.map +1 -1
- package/dist/core/provider-display-names.js +0 -1
- package/dist/core/provider-display-names.js.map +1 -1
- package/dist/core/session-manager.d.ts +0 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +0 -22
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -16
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +13 -43
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +0 -5
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/acp-mcp.d.ts +6 -0
- package/dist/core/tools/acp-mcp.d.ts.map +1 -0
- package/dist/core/tools/acp-mcp.js +86 -0
- package/dist/core/tools/acp-mcp.js.map +1 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ipython.d.ts.map +1 -1
- package/dist/core/tools/ipython.js +9 -4
- package/dist/core/tools/ipython.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -11
- package/dist/main.js.map +1 -1
- package/dist/modes/acp/acp-mode.d.ts.map +1 -1
- package/dist/modes/acp/acp-mode.js +1 -1
- package/dist/modes/acp/acp-mode.js.map +1 -1
- package/dist/modes/agent-connection/daemon-agent-connection.d.ts +0 -2
- package/dist/modes/agent-connection/daemon-agent-connection.d.ts.map +1 -1
- package/dist/modes/agent-connection/daemon-agent-connection.js +0 -2
- package/dist/modes/agent-connection/daemon-agent-connection.js.map +1 -1
- package/dist/modes/agents-view/agents-view-mode.d.ts.map +1 -1
- package/dist/modes/agents-view/agents-view-mode.js +0 -19
- package/dist/modes/agents-view/agents-view-mode.js.map +1 -1
- package/dist/modes/daemon/daemon-mode.d.ts +1 -10
- package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-mode.js +134 -274
- package/dist/modes/daemon/daemon-mode.js.map +1 -1
- package/dist/modes/daemon/daemon-protocol.d.ts +11 -5
- package/dist/modes/daemon/daemon-protocol.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-protocol.js +6 -8
- package/dist/modes/daemon/daemon-protocol.js.map +1 -1
- package/dist/modes/daemon/daemon-supervisor.d.ts +0 -3
- package/dist/modes/daemon/daemon-supervisor.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-supervisor.js +18 -52
- package/dist/modes/daemon/daemon-supervisor.js.map +1 -1
- package/dist/modes/daemon/daemon-worker-protocol.d.ts +1 -6
- package/dist/modes/daemon/daemon-worker-protocol.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-worker-protocol.js +0 -2
- package/dist/modes/daemon/daemon-worker-protocol.js.map +1 -1
- package/dist/modes/interactive/auth-flows.d.ts +0 -2
- package/dist/modes/interactive/auth-flows.d.ts.map +1 -1
- package/dist/modes/interactive/auth-flows.js +1 -101
- package/dist/modes/interactive/auth-flows.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +6 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +13 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +2 -2
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/mermaid.d.ts +12 -0
- package/dist/modes/interactive/components/mermaid.d.ts.map +1 -0
- package/dist/modes/interactive/components/mermaid.js +63 -0
- package/dist/modes/interactive/components/mermaid.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/feature-hints.d.ts.map +1 -1
- package/dist/modes/interactive/feature-hints.js +0 -4
- package/dist/modes/interactive/feature-hints.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -7
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +23 -279
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +1 -1
- package/dist/utils/version-check.js.map +1 -1
- package/docs/settings.md +1 -31
- package/docs/usage.md +1 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/gpt-6-astra.models.json +24 -0
- package/package.json +6 -5
- package/dist/core/agent-traces.d.ts +0 -130
- package/dist/core/agent-traces.d.ts.map +0 -1
- package/dist/core/agent-traces.js +0 -746
- package/dist/core/agent-traces.js.map +0 -1
- package/dist/core/telemetry.d.ts +0 -92
- package/dist/core/telemetry.d.ts.map +0 -1
- package/dist/core/telemetry.js +0 -633
- package/dist/core/telemetry.js.map +0 -1
|
@@ -6,15 +6,44 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
6
6
|
import { v4 as uuid } from "uuid";
|
|
7
7
|
import { reapKernelOrphanProcesses, recordOrphanProcessState } from "../orphan-process-journal.js";
|
|
8
8
|
import { ensureKernelPython } from "./bootstrap.js";
|
|
9
|
-
import { AGENT_MESSAGE_DISPLAY_MIME, ATTACHMENT_DISPLAY_MIME, createDeferred, createKernelStartupAbortError, DEFAULT_MAX_OUTPUT_CHARS, DEFAULT_SNAPSHOT_DEBOUNCE_MS, DIFF_DISPLAY_MIME, errorMessage,
|
|
9
|
+
import { AGENT_MESSAGE_DISPLAY_MIME, ATTACHMENT_DISPLAY_MIME, createDeferred, createKernelStartupAbortError, DEFAULT_MAX_OUTPUT_CHARS, DEFAULT_SNAPSHOT_DEBOUNCE_MS, DIFF_DISPLAY_MIME, errorMessage, HOST_REQUEST_SHUTDOWN_TIMEOUT_MS, installSignalHandlersOnce, isRecord, KERNEL_ABORT_GRACE_MS, KERNEL_BUSY_INTERRUPT_INTERVAL_MS, KERNEL_BUSY_REUSE_WAIT_MS, KERNEL_SHUTDOWN_TIMEOUT_MS, KernelBusyAfterInterruptError, liveKernels, MAX_ATTACHMENT_DATA_CHARS, MAX_LATE_SENT_AGENT_MESSAGE_HANDLERS, parseAttachmentDisplay, parseDiffDisplay, parseSentAgentMessage, raceStartupWithAbort, SNAPSHOT_EXECUTION_TIMEOUT_MS, } from "./shared.js";
|
|
10
10
|
import { DEFAULT_SNAPSHOT_MAX_BYTES, DEFAULT_SNAPSHOT_MAX_VARIABLE_BYTES, } from "./state-snapshot.js";
|
|
11
|
-
const REPL_PROTOCOL_VERSION =
|
|
11
|
+
const REPL_PROTOCOL_VERSION = 3;
|
|
12
12
|
const READY_TIMEOUT_MS = 30_000;
|
|
13
|
+
const REPAIR_STEP_TIMEOUT_MS = 30_000;
|
|
13
14
|
// Runtime-minted host-request ids never repeat; the bound only guards a
|
|
14
15
|
// misbehaving runtime from growing the dedup set forever.
|
|
15
16
|
const MAX_HANDLED_HOST_REQUEST_IDS = 1024;
|
|
16
17
|
// Cap for unattributed background output buffered between and during cells.
|
|
17
18
|
const MAX_BACKGROUND_OUTPUT_CHARS = 64 * 1024;
|
|
19
|
+
// Complete event vocabulary of protocol version 2 (see asm-agent-runtime/src/rlm/repl.md).
|
|
20
|
+
// The version handshake is exact, so an unknown kind is corruption, not a newer runtime.
|
|
21
|
+
const PROTOCOL_EVENT_KINDS = new Set([
|
|
22
|
+
"ready",
|
|
23
|
+
"stdout",
|
|
24
|
+
"stderr",
|
|
25
|
+
"result",
|
|
26
|
+
"display",
|
|
27
|
+
"host_request",
|
|
28
|
+
"error",
|
|
29
|
+
"done",
|
|
30
|
+
]);
|
|
31
|
+
/**
|
|
32
|
+
* Reason a JSON object still isn't a valid protocol frame, or undefined.
|
|
33
|
+
* `done` and `host_request` route strictly by non-empty string id (the runtime
|
|
34
|
+
* mints uuid hex ids and echoes the host's uuids); silently dropping an id-less
|
|
35
|
+
* one would leave the awaiting request unsettled forever.
|
|
36
|
+
*/
|
|
37
|
+
function invalidProtocolFrameReason(event) {
|
|
38
|
+
if (typeof event.event !== "string" || !PROTOCOL_EVENT_KINDS.has(event.event)) {
|
|
39
|
+
return "unknown protocol event";
|
|
40
|
+
}
|
|
41
|
+
if ((event.event === "done" || event.event === "host_request") &&
|
|
42
|
+
(typeof event.id !== "string" || event.id === "")) {
|
|
43
|
+
return `${event.event} frame without id`;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
18
47
|
function asStringArray(value) {
|
|
19
48
|
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
20
49
|
}
|
|
@@ -54,10 +83,26 @@ export class ReplKernelManager {
|
|
|
54
83
|
startGeneration = 0;
|
|
55
84
|
/** Generation whose graceful shutdown() owns the teardown, so the exit handler must not run it. */
|
|
56
85
|
gracefulShutdownGeneration;
|
|
86
|
+
gracefulShutdownPromise;
|
|
57
87
|
/** Memoized so concurrent callers all await the same in-flight startup. */
|
|
58
88
|
startPromise;
|
|
59
89
|
/** Pending debounced auto-snapshot, if one has been scheduled. */
|
|
60
90
|
snapshotTimer;
|
|
91
|
+
/** While the final dispose snapshot is flushing, new external executions are rejected. */
|
|
92
|
+
flushingSnapshotForDispose = false;
|
|
93
|
+
/** In-flight final snapshot flush; concurrent teardowns join it instead of re-flushing. */
|
|
94
|
+
snapshotFlushForDispose;
|
|
95
|
+
/** Repairs a child whose dedicated protocol stream emitted an invalid frame. */
|
|
96
|
+
protocolRepairPromise;
|
|
97
|
+
protocolRepairOwner;
|
|
98
|
+
/** Corruption seen while still "starting" (e.g. ready and garbage in one chunk) fails that start. */
|
|
99
|
+
startupProtocolError;
|
|
100
|
+
/** A repair discarded its kernel: the next fresh start must re-run the runtime bootstrap. */
|
|
101
|
+
pendingRebootstrap = false;
|
|
102
|
+
/** Restore the saved namespace on that fresh start too (false when the snapshot itself is the declared culprit). */
|
|
103
|
+
pendingRestore = false;
|
|
104
|
+
rebootstrapPromise;
|
|
105
|
+
teardownInFlight = 0;
|
|
61
106
|
constructor(options) {
|
|
62
107
|
this.options = {
|
|
63
108
|
python: options.python,
|
|
@@ -67,6 +112,7 @@ export class ReplKernelManager {
|
|
|
67
112
|
hostHandlers: options.hostHandlers,
|
|
68
113
|
pythonSkills: options.pythonSkills,
|
|
69
114
|
snapshot: options.snapshot,
|
|
115
|
+
bootstrapCode: options.bootstrapCode,
|
|
70
116
|
};
|
|
71
117
|
}
|
|
72
118
|
get ownerSessionId() {
|
|
@@ -137,11 +183,17 @@ export class ReplKernelManager {
|
|
|
137
183
|
if (child.pid !== undefined)
|
|
138
184
|
recordOrphanProcessState(child.pid, true);
|
|
139
185
|
this.readyDeferred = createDeferred();
|
|
186
|
+
this.startupProtocolError = undefined;
|
|
140
187
|
this.wireChild(child);
|
|
141
188
|
try {
|
|
142
189
|
const protocol = await this.waitForReady(child);
|
|
143
190
|
if (this.startStale(generation))
|
|
144
191
|
throw new Error("Kernel start superseded");
|
|
192
|
+
// Ready and a corrupt frame can share one stdout chunk: ready resolved the
|
|
193
|
+
// deferred synchronously before the corruption was parsed, so the rejection
|
|
194
|
+
// in failProtocolFrame was a no-op. Never mark such a child running.
|
|
195
|
+
if (this.startupProtocolError)
|
|
196
|
+
throw this.startupProtocolError;
|
|
145
197
|
if (protocol !== REPL_PROTOCOL_VERSION) {
|
|
146
198
|
throw new Error(`Kernel runtime speaks protocol ${protocol}, expected ${REPL_PROTOCOL_VERSION}. ` +
|
|
147
199
|
"Update asm-agent-runtime in the kernel Python (ASM_AGENT_KERNEL_PYTHON) to match this asm-agent.");
|
|
@@ -182,11 +234,19 @@ export class ReplKernelManager {
|
|
|
182
234
|
event = JSON.parse(line);
|
|
183
235
|
}
|
|
184
236
|
catch {
|
|
185
|
-
this.
|
|
186
|
-
|
|
237
|
+
this.failProtocolFrame(child, `unparseable protocol line: ${line.slice(0, 200)}`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!isRecord(event)) {
|
|
241
|
+
this.failProtocolFrame(child, `non-object protocol line: ${line.slice(0, 200)}`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const invalidReason = invalidProtocolFrameReason(event);
|
|
245
|
+
if (invalidReason) {
|
|
246
|
+
this.failProtocolFrame(child, `${invalidReason}: ${line.slice(0, 200)}`);
|
|
247
|
+
return;
|
|
187
248
|
}
|
|
188
|
-
|
|
189
|
-
this.handleEvent(event);
|
|
249
|
+
this.handleEvent(event);
|
|
190
250
|
}
|
|
191
251
|
});
|
|
192
252
|
child.stderr?.on("data", (buf) => {
|
|
@@ -220,6 +280,228 @@ export class ReplKernelManager {
|
|
|
220
280
|
this.cleanupResources();
|
|
221
281
|
});
|
|
222
282
|
}
|
|
283
|
+
failProtocolFrame(child, diagnostic) {
|
|
284
|
+
if (this.child !== child)
|
|
285
|
+
return;
|
|
286
|
+
this.appendKernelDiagnostic(diagnostic);
|
|
287
|
+
const error = new Error(`Kernel protocol error: ${diagnostic}`);
|
|
288
|
+
if (this.state === "starting")
|
|
289
|
+
this.startupProtocolError = error;
|
|
290
|
+
this.readyDeferred?.reject(error);
|
|
291
|
+
this.rejectActiveExecution(error);
|
|
292
|
+
if (this.teardownInFlight > 0 || this.state !== "running")
|
|
293
|
+
return;
|
|
294
|
+
if (this.protocolRepairOwner) {
|
|
295
|
+
// A repair's own replacement child corrupted: discard it instead of respawn-looping.
|
|
296
|
+
this.appendKernelDiagnostic("replacement kernel corrupted during protocol repair; giving up");
|
|
297
|
+
this.protocolRepairOwner.superseded = true;
|
|
298
|
+
// performRestore clears pendingRestore, so it still being set means the
|
|
299
|
+
// corruption struck at or before the restore phase: the snapshot stays
|
|
300
|
+
// the prime suspect (ambiguous attribution, loop-safe — retrying it
|
|
301
|
+
// would re-trigger the corruption). Corruption strictly after a
|
|
302
|
+
// successful restore never implicates the snapshot; keeping the flag
|
|
303
|
+
// costs at most one bounded restore per later attempt.
|
|
304
|
+
const snapshotSuspect = this.pendingRestore;
|
|
305
|
+
this.killChildToIdle();
|
|
306
|
+
if (snapshotSuspect)
|
|
307
|
+
this.pendingRestore = false;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const owner = { superseded: false };
|
|
311
|
+
this.protocolRepairOwner = owner;
|
|
312
|
+
const repair = this.repairProtocolChild(child, owner);
|
|
313
|
+
this.protocolRepairPromise = repair;
|
|
314
|
+
void repair.then(() => {
|
|
315
|
+
if (this.protocolRepairPromise === repair)
|
|
316
|
+
this.protocolRepairPromise = undefined;
|
|
317
|
+
if (this.protocolRepairOwner === owner)
|
|
318
|
+
this.protocolRepairOwner = undefined;
|
|
319
|
+
}, (repairError) => {
|
|
320
|
+
this.appendKernelDiagnostic(`protocol repair failed: ${errorMessage(repairError)}`);
|
|
321
|
+
if (this.protocolRepairPromise === repair)
|
|
322
|
+
this.protocolRepairPromise = undefined;
|
|
323
|
+
if (this.protocolRepairOwner === owner)
|
|
324
|
+
this.protocolRepairOwner = undefined;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async repairProtocolChild(child, owner) {
|
|
328
|
+
if (this.child !== child || this.state === "shutdown")
|
|
329
|
+
return;
|
|
330
|
+
this.killChildToIdle();
|
|
331
|
+
const start = this.start();
|
|
332
|
+
const generation = this.startGeneration;
|
|
333
|
+
try {
|
|
334
|
+
await start;
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
this.finishFailedProtocolRepair(owner, error);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (this.startStale(generation) || this.state !== "running") {
|
|
341
|
+
this.finishFailedProtocolRepair(owner);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const restored = await this.performRestore(true);
|
|
345
|
+
if (this.startStale(generation) || this.state !== "running") {
|
|
346
|
+
this.finishFailedProtocolRepair(owner);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (this.options.snapshot && restored === null) {
|
|
350
|
+
if (owner.superseded || this.protocolRepairOwner !== owner)
|
|
351
|
+
return;
|
|
352
|
+
this.appendKernelDiagnostic("protocol repair restore failed; discarding replacement kernel");
|
|
353
|
+
this.killChildToIdle();
|
|
354
|
+
// The snapshot is the declared culprit; the lazy path must not retry it.
|
|
355
|
+
this.pendingRestore = false;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
// Restore revives only the user namespace; live handles (rlm, bash, skills)
|
|
359
|
+
// come from the runtime bootstrap, so a repaired kernel must re-run it.
|
|
360
|
+
if (!this.options.bootstrapCode)
|
|
361
|
+
return;
|
|
362
|
+
const bootstrapped = await this.bootstrapRepairedKernel(this.options.bootstrapCode);
|
|
363
|
+
if (this.startStale(generation) || this.state !== "running") {
|
|
364
|
+
this.finishFailedProtocolRepair(owner);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (!bootstrapped) {
|
|
368
|
+
if (owner.superseded || this.protocolRepairOwner !== owner)
|
|
369
|
+
return;
|
|
370
|
+
this.appendKernelDiagnostic("protocol repair bootstrap failed; discarding replacement kernel");
|
|
371
|
+
this.killChildToIdle();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/** Bounded bootstrap of a repaired kernel; false when it failed. Never throws. */
|
|
375
|
+
async bootstrapRepairedKernel(code) {
|
|
376
|
+
try {
|
|
377
|
+
const r = await this.enqueueRequest({ type: "execute", code }, code, { internal: true, protocolRepair: true }, REPAIR_STEP_TIMEOUT_MS);
|
|
378
|
+
if (r.status !== "ok") {
|
|
379
|
+
this.appendKernelDiagnostic(`protocol repair bootstrap ${r.status === "aborted" ? "timed out" : "failed"}: ${r.error?.evalue ?? r.stderr}`);
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
this.pendingRebootstrap = false;
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
this.appendKernelDiagnostic(`protocol repair bootstrap error: ${errorMessage(error)}`);
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* A fresh kernel started after a discarded repair has none of the runtime
|
|
392
|
+
* bootstrap's live handles (rlm, bash, skills) and an empty namespace:
|
|
393
|
+
* reprovision (restore, then bootstrap) before any user request. A failed
|
|
394
|
+
* re-bootstrap discards the kernel again instead of serving user code on an
|
|
395
|
+
* unprovisioned namespace.
|
|
396
|
+
*/
|
|
397
|
+
async ensureKernelRebootstrapped(signal) {
|
|
398
|
+
const code = this.options.bootstrapCode;
|
|
399
|
+
const needsRestore = Boolean(this.options.snapshot) && this.pendingRestore;
|
|
400
|
+
const needsBootstrap = Boolean(code) && this.pendingRebootstrap;
|
|
401
|
+
// An in-flight repair owns its kernel's restore/bootstrap sequence, and
|
|
402
|
+
// a teardown's final snapshot must never trigger reprovisioning.
|
|
403
|
+
if ((!needsRestore && !needsBootstrap) ||
|
|
404
|
+
this.protocolRepairPromise ||
|
|
405
|
+
this.teardownInFlight > 0 ||
|
|
406
|
+
this.state !== "running") {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
let task = this.rebootstrapPromise;
|
|
410
|
+
if (!task) {
|
|
411
|
+
const started = this.reprovisionFreshKernel(code);
|
|
412
|
+
task = started;
|
|
413
|
+
this.rebootstrapPromise = started;
|
|
414
|
+
void started.finally(() => {
|
|
415
|
+
if (this.rebootstrapPromise === started)
|
|
416
|
+
this.rebootstrapPromise = undefined;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
// An aborted request never executes, so it may skip the wait; race the
|
|
420
|
+
// signal like waitForProtocolRepair does instead of riding out the
|
|
421
|
+
// bootstrap bound after a mid-wait abort.
|
|
422
|
+
if (signal) {
|
|
423
|
+
if (signal.aborted)
|
|
424
|
+
return;
|
|
425
|
+
let onAbort = () => { };
|
|
426
|
+
const aborted = new Promise((resolve) => {
|
|
427
|
+
onAbort = resolve;
|
|
428
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
429
|
+
});
|
|
430
|
+
try {
|
|
431
|
+
await Promise.race([task, aborted]);
|
|
432
|
+
}
|
|
433
|
+
finally {
|
|
434
|
+
signal.removeEventListener("abort", onAbort);
|
|
435
|
+
}
|
|
436
|
+
if (signal.aborted)
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const ok = await task; // bounded by REPAIR_STEP_TIMEOUT_MS
|
|
440
|
+
if (!ok)
|
|
441
|
+
throw new Error("Kernel bootstrap failed after protocol repair");
|
|
442
|
+
}
|
|
443
|
+
/** Restore (one-shot, best-effort) then bootstrap the lazily started fresh kernel. */
|
|
444
|
+
async reprovisionFreshKernel(code) {
|
|
445
|
+
if (this.options.snapshot && this.pendingRestore) {
|
|
446
|
+
await this.performRestore(true); // clears pendingRestore on success
|
|
447
|
+
// Corrupted during the restore: the spawned repair owns the kernel now.
|
|
448
|
+
if (this.protocolRepairPromise || this.state !== "running")
|
|
449
|
+
return false;
|
|
450
|
+
// One attempt per discard: a clean restore failure falls back to an
|
|
451
|
+
// empty namespace (ordinary startup semantics), never a retry loop.
|
|
452
|
+
this.pendingRestore = false;
|
|
453
|
+
}
|
|
454
|
+
if (!code || !this.pendingRebootstrap)
|
|
455
|
+
return true;
|
|
456
|
+
const ok = await this.bootstrapRepairedKernel(code);
|
|
457
|
+
if (!ok && this.state === "running")
|
|
458
|
+
this.killChildToIdle();
|
|
459
|
+
return ok;
|
|
460
|
+
}
|
|
461
|
+
/** Kill the current child and settle at clean idle, so the next start spawns fresh. */
|
|
462
|
+
killChildToIdle() {
|
|
463
|
+
// The discarded kernel carried the runtime bootstrap and (possibly) the
|
|
464
|
+
// restored namespace; a lazily started replacement must reprovision both.
|
|
465
|
+
this.pendingRebootstrap = true;
|
|
466
|
+
this.pendingRestore = true;
|
|
467
|
+
this.state = "shutdown";
|
|
468
|
+
liveKernels.delete(this);
|
|
469
|
+
this.cleanupResources("SIGKILL");
|
|
470
|
+
this.state = "idle";
|
|
471
|
+
}
|
|
472
|
+
finishFailedProtocolRepair(owner, error) {
|
|
473
|
+
if (error)
|
|
474
|
+
this.appendKernelDiagnostic(`protocol repair start failed: ${errorMessage(error)}`);
|
|
475
|
+
if (owner.superseded || this.protocolRepairOwner !== owner)
|
|
476
|
+
return;
|
|
477
|
+
if (this.state === "shutdown")
|
|
478
|
+
this.state = "idle";
|
|
479
|
+
}
|
|
480
|
+
supersedeProtocolRepair() {
|
|
481
|
+
if (this.protocolRepairOwner)
|
|
482
|
+
this.protocolRepairOwner.superseded = true;
|
|
483
|
+
}
|
|
484
|
+
/** Wait until no protocol repair is pending; resolves early when the signal aborts. */
|
|
485
|
+
async waitForProtocolRepair(signal) {
|
|
486
|
+
while (this.protocolRepairPromise && !signal?.aborted) {
|
|
487
|
+
const repair = this.protocolRepairPromise;
|
|
488
|
+
if (!signal) {
|
|
489
|
+
await repair;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
let onAbort = () => { };
|
|
493
|
+
const aborted = new Promise((resolve) => {
|
|
494
|
+
onAbort = resolve;
|
|
495
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
496
|
+
});
|
|
497
|
+
try {
|
|
498
|
+
await Promise.race([repair, aborted]);
|
|
499
|
+
}
|
|
500
|
+
finally {
|
|
501
|
+
signal.removeEventListener("abort", onAbort);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
223
505
|
async waitForReady(child) {
|
|
224
506
|
const ready = this.readyDeferred;
|
|
225
507
|
if (!ready)
|
|
@@ -368,6 +650,7 @@ export class ReplKernelManager {
|
|
|
368
650
|
}
|
|
369
651
|
}
|
|
370
652
|
async execute(code, opts = {}) {
|
|
653
|
+
await this.waitForProtocolRepair(opts.signal);
|
|
371
654
|
const result = await this.enqueueExecute(code, opts);
|
|
372
655
|
// Refresh the on-disk snapshot after real work so a later resume (or a
|
|
373
656
|
// crash before graceful shutdown) revives the most recent namespace.
|
|
@@ -389,6 +672,22 @@ export class ReplKernelManager {
|
|
|
389
672
|
if (this.state === "shutdown") {
|
|
390
673
|
throw new Error("Kernel has been shut down");
|
|
391
674
|
}
|
|
675
|
+
if (this.flushingSnapshotForDispose && !opts.internal) {
|
|
676
|
+
throw new Error("Kernel is shutting down");
|
|
677
|
+
}
|
|
678
|
+
if (!opts.protocolRepair)
|
|
679
|
+
await this.ensureKernelRebootstrapped(opts.signal);
|
|
680
|
+
// Aborted while waiting on the re-bootstrap: settle now instead of parking
|
|
681
|
+
// on the queue slot behind the still-running bootstrap.
|
|
682
|
+
if (opts.signal?.aborted) {
|
|
683
|
+
return { stdout: "", stderr: "", status: "aborted", durationMs: 0 };
|
|
684
|
+
}
|
|
685
|
+
// Re-check: a final flush may have started while this request awaited the
|
|
686
|
+
// lazy re-bootstrap; admitting it now would splice it between the flush's
|
|
687
|
+
// captured queue and the final snapshot, unbounding the teardown.
|
|
688
|
+
if (this.flushingSnapshotForDispose && !opts.internal) {
|
|
689
|
+
throw new Error("Kernel is shutting down");
|
|
690
|
+
}
|
|
392
691
|
const prev = this.executionQueue;
|
|
393
692
|
let resolveNext = () => { };
|
|
394
693
|
this.executionQueue = new Promise((r) => {
|
|
@@ -405,6 +704,13 @@ export class ReplKernelManager {
|
|
|
405
704
|
if (this.state === "shutdown") {
|
|
406
705
|
throw new Error("Kernel has been shut down");
|
|
407
706
|
}
|
|
707
|
+
// A repair started while this request was queued or busy-waiting: release
|
|
708
|
+
// the slot so the repair's own restore can run, then requeue behind it.
|
|
709
|
+
if (this.protocolRepairPromise && !opts.protocolRepair) {
|
|
710
|
+
resolveNext();
|
|
711
|
+
await this.waitForProtocolRepair(opts.signal);
|
|
712
|
+
return this.enqueueRequest(requestFields, code, opts, executionTimeoutMs);
|
|
713
|
+
}
|
|
408
714
|
if (executionTimeoutMs === undefined) {
|
|
409
715
|
return await this.executeInner(requestFields, code, opts, started);
|
|
410
716
|
}
|
|
@@ -683,7 +989,7 @@ export class ReplKernelManager {
|
|
|
683
989
|
try {
|
|
684
990
|
const result = await this.handleHostRequest(data);
|
|
685
991
|
try {
|
|
686
|
-
await this.writeLine({ type: "host_reply", id: requestId, data: { status: "ok",
|
|
992
|
+
await this.writeLine({ type: "host_reply", id: requestId, data: { status: "ok", result } });
|
|
687
993
|
}
|
|
688
994
|
catch (replyError) {
|
|
689
995
|
this.appendKernelDiagnostic(`failed to send host request ok reply for ${requestId}: ${errorMessage(replyError)}`);
|
|
@@ -786,51 +1092,76 @@ export class ReplKernelManager {
|
|
|
786
1092
|
globalThis.clearTimeout(timeout);
|
|
787
1093
|
}
|
|
788
1094
|
if (result === "timeout") {
|
|
789
|
-
this.appendKernelDiagnostic(`timed out waiting ${timeoutMs}ms for ${tasks.length} host request task(s) during
|
|
1095
|
+
this.appendKernelDiagnostic(`timed out waiting ${timeoutMs}ms for ${tasks.length} host request task(s) during shutdown`);
|
|
790
1096
|
}
|
|
791
1097
|
}
|
|
792
|
-
/** Resolves true when this call performed the cleanup (false: a concurrent teardown won). */
|
|
1098
|
+
/** Resolves true when this call performed the cleanup (false: a concurrent teardown won; a joiner's options are ignored - the first caller's policy wins). */
|
|
793
1099
|
async shutdown(opts = {}) {
|
|
1100
|
+
const inFlightShutdown = this.gracefulShutdownPromise;
|
|
1101
|
+
if (inFlightShutdown) {
|
|
1102
|
+
await inFlightShutdown;
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
this.teardownInFlight++;
|
|
1106
|
+
this.supersedeProtocolRepair();
|
|
1107
|
+
const operation = this.performShutdown(opts);
|
|
1108
|
+
this.gracefulShutdownPromise = operation;
|
|
1109
|
+
try {
|
|
1110
|
+
return await operation;
|
|
1111
|
+
}
|
|
1112
|
+
finally {
|
|
1113
|
+
this.teardownInFlight--;
|
|
1114
|
+
if (this.gracefulShutdownPromise === operation)
|
|
1115
|
+
this.gracefulShutdownPromise = undefined;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
async performShutdown(opts) {
|
|
794
1119
|
if (this.state === "shutdown") {
|
|
795
1120
|
liveKernels.delete(this);
|
|
1121
|
+
if (this.gracefulShutdownGeneration === this.startGeneration)
|
|
1122
|
+
return false;
|
|
796
1123
|
this.cleanupResources();
|
|
797
1124
|
return true;
|
|
798
1125
|
}
|
|
799
1126
|
// Captured before any await: teardowns and newer starts bump the counter.
|
|
800
1127
|
const generation = this.startGeneration;
|
|
801
|
-
// Best-effort final flush (bounded) before teardown — used by signal handlers
|
|
802
|
-
// so a SIGINT/SIGTERM exit doesn't lose work the debounced snapshot hasn't saved.
|
|
803
1128
|
if (opts.snapshot) {
|
|
804
1129
|
await this.flushSnapshotForDispose();
|
|
805
1130
|
if (this.startStale(generation))
|
|
806
|
-
return false;
|
|
1131
|
+
return false;
|
|
807
1132
|
}
|
|
1133
|
+
// Protocol shutdown first: the runtime closes MCP servers and kills live bash() process groups a bare hard-kill would leak.
|
|
1134
|
+
const protocolShutdownAvailable = this.state === "running";
|
|
808
1135
|
this.state = "shutdown";
|
|
809
1136
|
liveKernels.delete(this);
|
|
810
|
-
// Claim the teardown: our own child's exit handler must not run cleanupResources
|
|
811
|
-
// (which bumps the generation and would misread this call as superseded). A
|
|
812
|
-
// concurrent kill()/dispose() still bumps the generation and supersedes us.
|
|
813
1137
|
this.gracefulShutdownGeneration = generation;
|
|
814
1138
|
let shutdownTimer;
|
|
815
1139
|
let doneWaiterId;
|
|
816
1140
|
let performedCleanup = false;
|
|
817
|
-
const shutdownDeadline = new Promise((_resolve, reject) => {
|
|
818
|
-
shutdownTimer = globalThis.setTimeout(() => reject(new Error(`Kernel did not shut down within ${KERNEL_SHUTDOWN_TIMEOUT_MS}ms`)), KERNEL_SHUTDOWN_TIMEOUT_MS);
|
|
819
|
-
shutdownTimer.unref?.();
|
|
820
|
-
});
|
|
821
1141
|
try {
|
|
822
|
-
if (
|
|
1142
|
+
if (opts.drainHostRequests) {
|
|
1143
|
+
const inFlightHostRequests = [...this.inFlightHostRequests];
|
|
1144
|
+
if (inFlightHostRequests.length > 0) {
|
|
1145
|
+
await this.waitForHostRequestsToSettle(inFlightHostRequests, HOST_REQUEST_SHUTDOWN_TIMEOUT_MS);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (protocolShutdownAvailable &&
|
|
1149
|
+
!this.startStale(generation) &&
|
|
1150
|
+
this.child?.stdin &&
|
|
1151
|
+
!this.child.stdin.destroyed) {
|
|
823
1152
|
const requestId = uuid();
|
|
824
1153
|
doneWaiterId = requestId;
|
|
825
1154
|
const doneReply = new Promise((resolve) => {
|
|
826
1155
|
this.pendingDoneWaiters.set(requestId, resolve);
|
|
827
1156
|
});
|
|
1157
|
+
const shutdownDeadline = new Promise((_resolve, reject) => {
|
|
1158
|
+
shutdownTimer = globalThis.setTimeout(() => reject(new Error(`Kernel did not shut down within ${KERNEL_SHUTDOWN_TIMEOUT_MS}ms`)), KERNEL_SHUTDOWN_TIMEOUT_MS);
|
|
1159
|
+
shutdownTimer.unref?.();
|
|
1160
|
+
});
|
|
828
1161
|
const send = this.writeLine({ type: "shutdown", id: requestId });
|
|
829
1162
|
send.catch(() => undefined);
|
|
830
|
-
// A kernel that exits without delivering its shutdown done must not stall the deadline.
|
|
831
1163
|
const kernelExit = this.waitForKernelExit();
|
|
832
1164
|
const gracefulReply = Promise.all([send, doneReply]);
|
|
833
|
-
// Abandoned by the race, a late send failure must not reject unhandled.
|
|
834
1165
|
gracefulReply.catch(() => undefined);
|
|
835
1166
|
await Promise.race([gracefulReply, kernelExit, shutdownDeadline]);
|
|
836
1167
|
await Promise.race([kernelExit, shutdownDeadline]);
|
|
@@ -846,8 +1177,6 @@ export class ReplKernelManager {
|
|
|
846
1177
|
this.pendingDoneWaiters.delete(doneWaiterId);
|
|
847
1178
|
if (this.gracefulShutdownGeneration === generation)
|
|
848
1179
|
this.gracefulShutdownGeneration = undefined;
|
|
849
|
-
// A superseded shutdown must not tear down the newer start's kernel. Ownership is decided
|
|
850
|
-
// here, before cleanupResources bumps the generation and would misread this call as superseded.
|
|
851
1180
|
if (!this.startStale(generation)) {
|
|
852
1181
|
this.cleanupResources();
|
|
853
1182
|
performedCleanup = true;
|
|
@@ -856,6 +1185,12 @@ export class ReplKernelManager {
|
|
|
856
1185
|
return performedCleanup;
|
|
857
1186
|
}
|
|
858
1187
|
async restart() {
|
|
1188
|
+
// A final dispose flush owns the queue tail. Taking a slot now and joining
|
|
1189
|
+
// the in-flight shutdown would deadlock: the flush's snapshot waits on our
|
|
1190
|
+
// slot while we wait on the flush's shutdown.
|
|
1191
|
+
if (this.flushingSnapshotForDispose) {
|
|
1192
|
+
throw new Error("Kernel is shutting down");
|
|
1193
|
+
}
|
|
859
1194
|
const prev = this.executionQueue;
|
|
860
1195
|
let resolveNext = () => { };
|
|
861
1196
|
this.executionQueue = new Promise((r) => {
|
|
@@ -875,6 +1210,7 @@ export class ReplKernelManager {
|
|
|
875
1210
|
}
|
|
876
1211
|
}
|
|
877
1212
|
async kill() {
|
|
1213
|
+
this.supersedeProtocolRepair();
|
|
878
1214
|
this.state = "shutdown";
|
|
879
1215
|
liveKernels.delete(this);
|
|
880
1216
|
this.cleanupResources("SIGKILL");
|
|
@@ -927,15 +1263,20 @@ export class ReplKernelManager {
|
|
|
927
1263
|
* (rlm, skills) over anything restored. Never throws.
|
|
928
1264
|
*/
|
|
929
1265
|
async restoreState() {
|
|
1266
|
+
return this.performRestore(false);
|
|
1267
|
+
}
|
|
1268
|
+
/** Repair restores bypass the repair gate and are bounded so a stalled kernel cannot wedge it. */
|
|
1269
|
+
async performRestore(protocolRepair) {
|
|
930
1270
|
const cfg = this.options.snapshot;
|
|
931
1271
|
if (!cfg)
|
|
932
1272
|
return null;
|
|
933
1273
|
try {
|
|
934
|
-
const r = await this.enqueueRequest({ type: "restore", path: cfg.path }, "", { internal: true });
|
|
1274
|
+
const r = await this.enqueueRequest({ type: "restore", path: cfg.path }, "", { internal: true, protocolRepair }, protocolRepair ? REPAIR_STEP_TIMEOUT_MS : undefined);
|
|
935
1275
|
if (r.status !== "ok" || !r.doneFields) {
|
|
936
|
-
this.appendKernelDiagnostic(`state restore failed: ${r.error?.evalue ?? r.stderr}`);
|
|
1276
|
+
this.appendKernelDiagnostic(`state restore ${r.status === "aborted" ? "timed out" : "failed"}: ${r.error?.evalue ?? r.stderr}`);
|
|
937
1277
|
return null;
|
|
938
1278
|
}
|
|
1279
|
+
this.pendingRestore = false;
|
|
939
1280
|
return {
|
|
940
1281
|
restored: asStringArray(r.doneFields.restored),
|
|
941
1282
|
failed: asReasonArray(r.doneFields.failed),
|
|
@@ -984,92 +1325,50 @@ export class ReplKernelManager {
|
|
|
984
1325
|
this.snapshotTimer = undefined;
|
|
985
1326
|
}
|
|
986
1327
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
timeout = globalThis.setTimeout(resolve, SNAPSHOT_DISPOSE_TIMEOUT_MS);
|
|
994
|
-
if (timeout && typeof timeout === "object" && "unref" in timeout)
|
|
995
|
-
timeout.unref();
|
|
1328
|
+
flushSnapshotForDispose() {
|
|
1329
|
+
// Concurrent teardowns (dispose vs a signal-handler shutdown) join one flush:
|
|
1330
|
+
// a second flusher would clear the execution guard while the first is still
|
|
1331
|
+
// snapshotting and enqueue a duplicate final snapshot behind it.
|
|
1332
|
+
this.snapshotFlushForDispose ??= this.runSnapshotFlushForDispose().finally(() => {
|
|
1333
|
+
this.snapshotFlushForDispose = undefined;
|
|
996
1334
|
});
|
|
997
|
-
|
|
998
|
-
await Promise.race([this.snapshotState().then(() => undefined), guard]);
|
|
999
|
-
}
|
|
1000
|
-
finally {
|
|
1001
|
-
if (timeout)
|
|
1002
|
-
clearTimeout(timeout);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
/** Graceful cleanup. Waits briefly for in-flight host request handlers before killing the child. */
|
|
1006
|
-
dispose() {
|
|
1007
|
-
return (async () => {
|
|
1008
|
-
// Captured before any await: teardowns and newer starts bump the counter.
|
|
1009
|
-
const generation = this.startGeneration;
|
|
1010
|
-
// Final namespace flush while the kernel is still live (session end / reload).
|
|
1011
|
-
await this.flushSnapshotForDispose();
|
|
1012
|
-
if (this.startStale(generation))
|
|
1013
|
-
return; // superseded mid-flush: the newer owner already cleaned this kernel
|
|
1014
|
-
this.state = "shutdown";
|
|
1015
|
-
liveKernels.delete(this);
|
|
1016
|
-
// Claim the teardown so the child's exit handler does not run
|
|
1017
|
-
// cleanupResources mid-dispose (same contract as shutdown()).
|
|
1018
|
-
this.gracefulShutdownGeneration = generation;
|
|
1019
|
-
const inFlightHostRequests = [...this.inFlightHostRequests];
|
|
1020
|
-
try {
|
|
1021
|
-
if (inFlightHostRequests.length > 0) {
|
|
1022
|
-
await this.waitForHostRequestsToSettle(inFlightHostRequests, HOST_REQUEST_DISPOSE_TIMEOUT_MS);
|
|
1023
|
-
}
|
|
1024
|
-
if (!this.startStale(generation)) {
|
|
1025
|
-
// Bounded protocol shutdown first: the runtime's shutdown branch
|
|
1026
|
-
// closes MCP servers and kills live bash() process groups, which
|
|
1027
|
-
// a bare hard-kill would leak until the orphan reaper runs.
|
|
1028
|
-
await this.requestProtocolShutdown(KERNEL_SHUTDOWN_TIMEOUT_MS);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
finally {
|
|
1032
|
-
if (this.gracefulShutdownGeneration === generation)
|
|
1033
|
-
this.gracefulShutdownGeneration = undefined;
|
|
1034
|
-
if (!this.startStale(generation))
|
|
1035
|
-
this.cleanupResources(); // else: superseded, the newer owner already cleaned
|
|
1036
|
-
}
|
|
1037
|
-
})();
|
|
1335
|
+
return this.snapshotFlushForDispose;
|
|
1038
1336
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
const stdin = this.child?.stdin;
|
|
1042
|
-
if (!stdin || stdin.destroyed)
|
|
1337
|
+
async runSnapshotFlushForDispose() {
|
|
1338
|
+
if (!this.options.snapshot || !this.isRunning)
|
|
1043
1339
|
return;
|
|
1044
|
-
|
|
1045
|
-
|
|
1340
|
+
// A kernel that never restored the saved namespace must not overwrite it:
|
|
1341
|
+
// the on-disk snapshot is strictly fresher than this namespace.
|
|
1342
|
+
if (this.pendingRestore)
|
|
1343
|
+
return;
|
|
1344
|
+
// Block new external executions so none can splice ahead of the final snapshot and stall dispose.
|
|
1345
|
+
this.flushingSnapshotForDispose = true;
|
|
1046
1346
|
try {
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
// Best-effort: cleanupResources still hard-kills the child.
|
|
1347
|
+
const pendingExecutions = this.executionQueue;
|
|
1348
|
+
if (this.activeExecution)
|
|
1349
|
+
void this.interrupt().catch(() => undefined);
|
|
1350
|
+
let timeout;
|
|
1351
|
+
const queueSettled = await Promise.race([
|
|
1352
|
+
pendingExecutions.then(() => true),
|
|
1353
|
+
new Promise((resolve) => {
|
|
1354
|
+
timeout = globalThis.setTimeout(() => resolve(false), SNAPSHOT_EXECUTION_TIMEOUT_MS);
|
|
1355
|
+
timeout.unref?.();
|
|
1356
|
+
}),
|
|
1357
|
+
]);
|
|
1358
|
+
if (timeout)
|
|
1359
|
+
globalThis.clearTimeout(timeout);
|
|
1360
|
+
if (!queueSettled)
|
|
1361
|
+
return;
|
|
1362
|
+
await this.captureSnapshot({ executionTimeoutMs: SNAPSHOT_EXECUTION_TIMEOUT_MS });
|
|
1064
1363
|
}
|
|
1065
1364
|
finally {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
this.pendingDoneWaiters.delete(requestId);
|
|
1365
|
+
// Reset: a superseding start() can revive this kernel for new work.
|
|
1366
|
+
this.flushingSnapshotForDispose = false;
|
|
1069
1367
|
}
|
|
1070
1368
|
}
|
|
1071
1369
|
/** Synchronous best-effort cleanup. Safe to call from `process.on('exit')`. */
|
|
1072
1370
|
disposeSync() {
|
|
1371
|
+
this.supersedeProtocolRepair();
|
|
1073
1372
|
this.state = "shutdown";
|
|
1074
1373
|
liveKernels.delete(this);
|
|
1075
1374
|
this.cleanupResources();
|