@asm-agent/coding-agent 0.8.8 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +2 -7
  3. package/dist/asm-agent-runtime/src/__pycache__/asm_cm_driver.cpython-313.pyc +0 -0
  4. package/dist/asm-agent-runtime/src/__pycache__/phase76_address_head.cpython-313.pyc +0 -0
  5. package/dist/asm-agent-runtime/src/rlm/__init__.py +1 -1
  6. package/dist/asm-agent-runtime/src/rlm/__pycache__/__init__.cpython-312.pyc +0 -0
  7. package/dist/asm-agent-runtime/src/rlm/__pycache__/__init__.cpython-313.pyc +0 -0
  8. package/dist/asm-agent-runtime/src/rlm/__pycache__/bash.cpython-312.pyc +0 -0
  9. package/dist/asm-agent-runtime/src/rlm/__pycache__/bash.cpython-313.pyc +0 -0
  10. package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp.cpython-312.pyc +0 -0
  11. package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp_base.cpython-312.pyc +0 -0
  12. package/dist/asm-agent-runtime/src/rlm/__pycache__/repl.cpython-312.pyc +0 -0
  13. package/dist/asm-agent-runtime/src/rlm/__pycache__/repl.cpython-313.pyc +0 -0
  14. package/dist/asm-agent-runtime/src/rlm/__pycache__/skill.cpython-313.pyc +0 -0
  15. package/dist/asm-agent-runtime/src/rlm/bash.py +94 -31
  16. package/dist/asm-agent-runtime/src/rlm/repl.md +3 -3
  17. package/dist/asm-agent-runtime/src/rlm/repl.py +46 -45
  18. package/dist/asm-agent-runtime/test/__pycache__/test_bash.cpython-313.pyc +0 -0
  19. package/dist/asm-agent-runtime/test/__pycache__/test_repl.cpython-313.pyc +0 -0
  20. package/dist/asm-agent-runtime/test/test_bash.py +74 -19
  21. package/dist/asm-agent-runtime/test/test_repl.py +67 -3
  22. package/dist/asm-agent-runtime/uv.lock +446 -155
  23. package/dist/bundle/{anthropic-OFECORE7.js → anthropic-ZOZNPFXS.js} +2 -2
  24. package/dist/bundle/{azure-openai-responses-AOVTLR6N.js → azure-openai-responses-TDHCU647.js} +2 -2
  25. package/dist/bundle/{bundled-modules-JQCJDEIY.js → bundled-modules-Q2YODC4B.js} +3 -3
  26. package/dist/bundle/{chunk-M67RGWDQ.js → chunk-AMKLNEAK.js} +25 -44
  27. package/dist/bundle/{chunk-7TB7CGPV.js → chunk-FPY7FFJS.js} +978 -1821
  28. package/dist/bundle/{chunk-JC2TT4MZ.js → chunk-G6SC4YYW.js} +4942 -1916
  29. package/dist/bundle/{chunk-A65OM3TZ.js → chunk-QLHAP3FT.js} +1 -1
  30. package/dist/bundle/{cli-main-FNHDQGQD.js → cli-main-277AG2BR.js} +3 -3
  31. package/dist/bundle/cli.js +1 -1
  32. package/dist/bundle/{google-RVJL6RIQ.js → google-Q3AJWUOY.js} +1 -1
  33. package/dist/bundle/{google-vertex-EZTRRRH6.js → google-vertex-2CSNOUBS.js} +1 -1
  34. package/dist/bundle/{main-IYXIIAJN.js → main-TNXWJXIU.js} +3 -3
  35. package/dist/bundle/{mistral-FSGKSMA7.js → mistral-XRYJGLLG.js} +1 -1
  36. package/dist/bundle/{openai-codex-responses-VM5Y564D.js → openai-codex-responses-C6BRO2BN.js} +2 -2
  37. package/dist/bundle/{openai-completions-X46FOQDN.js → openai-completions-DNXCK2JW.js} +3 -3
  38. package/dist/bundle/{openai-responses-SOQNEPBE.js → openai-responses-BEMWVIXE.js} +2 -2
  39. package/dist/config.d.ts +0 -1
  40. package/dist/config.d.ts.map +1 -1
  41. package/dist/config.js +0 -3
  42. package/dist/config.js.map +1 -1
  43. package/dist/core/agent-session-config.d.ts +1 -2
  44. package/dist/core/agent-session-config.d.ts.map +1 -1
  45. package/dist/core/agent-session-config.js +0 -3
  46. package/dist/core/agent-session-config.js.map +1 -1
  47. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  48. package/dist/core/agent-session-runtime.js +0 -8
  49. package/dist/core/agent-session-runtime.js.map +1 -1
  50. package/dist/core/agent-session-services.d.ts +0 -2
  51. package/dist/core/agent-session-services.d.ts.map +1 -1
  52. package/dist/core/agent-session-services.js +0 -22
  53. package/dist/core/agent-session-services.js.map +1 -1
  54. package/dist/core/agent-session.d.ts +8 -1
  55. package/dist/core/agent-session.d.ts.map +1 -1
  56. package/dist/core/agent-session.js +100 -22
  57. package/dist/core/agent-session.js.map +1 -1
  58. package/dist/core/kernel/bootstrap.d.ts.map +1 -1
  59. package/dist/core/kernel/bootstrap.js +1 -1
  60. package/dist/core/kernel/bootstrap.js.map +1 -1
  61. package/dist/core/kernel/repl-manager.d.ts +32 -8
  62. package/dist/core/kernel/repl-manager.d.ts.map +1 -1
  63. package/dist/core/kernel/repl-manager.js +401 -102
  64. package/dist/core/kernel/repl-manager.js.map +1 -1
  65. package/dist/core/kernel/shared.d.ts +11 -7
  66. package/dist/core/kernel/shared.d.ts.map +1 -1
  67. package/dist/core/kernel/shared.js +2 -5
  68. package/dist/core/kernel/shared.js.map +1 -1
  69. package/dist/core/mcp/mcp-manager.d.ts +4 -2
  70. package/dist/core/mcp/mcp-manager.d.ts.map +1 -1
  71. package/dist/core/mcp/mcp-manager.js +9 -7
  72. package/dist/core/mcp/mcp-manager.js.map +1 -1
  73. package/dist/core/prime-inference-auth.d.ts +0 -9
  74. package/dist/core/prime-inference-auth.d.ts.map +1 -1
  75. package/dist/core/prime-inference-auth.js +0 -50
  76. package/dist/core/prime-inference-auth.js.map +1 -1
  77. package/dist/core/provider-display-names.d.ts.map +1 -1
  78. package/dist/core/provider-display-names.js +0 -1
  79. package/dist/core/provider-display-names.js.map +1 -1
  80. package/dist/core/session-manager.d.ts +0 -4
  81. package/dist/core/session-manager.d.ts.map +1 -1
  82. package/dist/core/session-manager.js +0 -22
  83. package/dist/core/session-manager.js.map +1 -1
  84. package/dist/core/settings-manager.d.ts +4 -16
  85. package/dist/core/settings-manager.d.ts.map +1 -1
  86. package/dist/core/settings-manager.js +13 -43
  87. package/dist/core/settings-manager.js.map +1 -1
  88. package/dist/core/slash-commands.d.ts.map +1 -1
  89. package/dist/core/slash-commands.js +0 -5
  90. package/dist/core/slash-commands.js.map +1 -1
  91. package/dist/core/tools/acp-mcp.d.ts +6 -0
  92. package/dist/core/tools/acp-mcp.d.ts.map +1 -0
  93. package/dist/core/tools/acp-mcp.js +86 -0
  94. package/dist/core/tools/acp-mcp.js.map +1 -0
  95. package/dist/core/tools/index.d.ts +1 -0
  96. package/dist/core/tools/index.d.ts.map +1 -1
  97. package/dist/core/tools/index.js +1 -0
  98. package/dist/core/tools/index.js.map +1 -1
  99. package/dist/core/tools/ipython.d.ts.map +1 -1
  100. package/dist/core/tools/ipython.js +9 -4
  101. package/dist/core/tools/ipython.js.map +1 -1
  102. package/dist/main.d.ts.map +1 -1
  103. package/dist/main.js +2 -11
  104. package/dist/main.js.map +1 -1
  105. package/dist/modes/acp/acp-mode.d.ts.map +1 -1
  106. package/dist/modes/acp/acp-mode.js +1 -1
  107. package/dist/modes/acp/acp-mode.js.map +1 -1
  108. package/dist/modes/agent-connection/daemon-agent-connection.d.ts +0 -2
  109. package/dist/modes/agent-connection/daemon-agent-connection.d.ts.map +1 -1
  110. package/dist/modes/agent-connection/daemon-agent-connection.js +0 -2
  111. package/dist/modes/agent-connection/daemon-agent-connection.js.map +1 -1
  112. package/dist/modes/agents-view/agents-view-mode.d.ts.map +1 -1
  113. package/dist/modes/agents-view/agents-view-mode.js +0 -19
  114. package/dist/modes/agents-view/agents-view-mode.js.map +1 -1
  115. package/dist/modes/daemon/daemon-mode.d.ts +1 -10
  116. package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
  117. package/dist/modes/daemon/daemon-mode.js +134 -274
  118. package/dist/modes/daemon/daemon-mode.js.map +1 -1
  119. package/dist/modes/daemon/daemon-protocol.d.ts +11 -5
  120. package/dist/modes/daemon/daemon-protocol.d.ts.map +1 -1
  121. package/dist/modes/daemon/daemon-protocol.js +6 -8
  122. package/dist/modes/daemon/daemon-protocol.js.map +1 -1
  123. package/dist/modes/daemon/daemon-supervisor.d.ts +0 -3
  124. package/dist/modes/daemon/daemon-supervisor.d.ts.map +1 -1
  125. package/dist/modes/daemon/daemon-supervisor.js +18 -52
  126. package/dist/modes/daemon/daemon-supervisor.js.map +1 -1
  127. package/dist/modes/daemon/daemon-worker-protocol.d.ts +1 -6
  128. package/dist/modes/daemon/daemon-worker-protocol.d.ts.map +1 -1
  129. package/dist/modes/daemon/daemon-worker-protocol.js +0 -2
  130. package/dist/modes/daemon/daemon-worker-protocol.js.map +1 -1
  131. package/dist/modes/interactive/auth-flows.d.ts +0 -2
  132. package/dist/modes/interactive/auth-flows.d.ts.map +1 -1
  133. package/dist/modes/interactive/auth-flows.js +1 -101
  134. package/dist/modes/interactive/auth-flows.js.map +1 -1
  135. package/dist/modes/interactive/components/assistant-message.d.ts +6 -1
  136. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  137. package/dist/modes/interactive/components/assistant-message.js +13 -3
  138. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  139. package/dist/modes/interactive/components/footer.d.ts +1 -1
  140. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  141. package/dist/modes/interactive/components/footer.js +2 -2
  142. package/dist/modes/interactive/components/footer.js.map +1 -1
  143. package/dist/modes/interactive/components/mermaid.d.ts +12 -0
  144. package/dist/modes/interactive/components/mermaid.d.ts.map +1 -0
  145. package/dist/modes/interactive/components/mermaid.js +63 -0
  146. package/dist/modes/interactive/components/mermaid.js.map +1 -0
  147. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  148. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  149. package/dist/modes/interactive/components/settings-selector.js +10 -0
  150. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  151. package/dist/modes/interactive/feature-hints.d.ts.map +1 -1
  152. package/dist/modes/interactive/feature-hints.js +0 -4
  153. package/dist/modes/interactive/feature-hints.js.map +1 -1
  154. package/dist/modes/interactive/interactive-mode.d.ts +1 -7
  155. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  156. package/dist/modes/interactive/interactive-mode.js +23 -279
  157. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  158. package/dist/utils/version-check.d.ts.map +1 -1
  159. package/dist/utils/version-check.js +1 -1
  160. package/dist/utils/version-check.js.map +1 -1
  161. package/docs/settings.md +1 -31
  162. package/docs/usage.md +1 -4
  163. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  164. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  165. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  166. package/examples/extensions/sandbox/package-lock.json +2 -2
  167. package/examples/extensions/sandbox/package.json +1 -1
  168. package/examples/extensions/with-deps/package-lock.json +2 -2
  169. package/examples/extensions/with-deps/package.json +1 -1
  170. package/package.json +6 -5
  171. package/dist/core/agent-traces.d.ts +0 -130
  172. package/dist/core/agent-traces.d.ts.map +0 -1
  173. package/dist/core/agent-traces.js +0 -746
  174. package/dist/core/agent-traces.js.map +0 -1
  175. package/dist/core/telemetry.d.ts +0 -92
  176. package/dist/core/telemetry.d.ts.map +0 -1
  177. package/dist/core/telemetry.js +0 -633
  178. 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, HOST_REQUEST_DISPOSE_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_DISPOSE_TIMEOUT_MS, SNAPSHOT_EXECUTION_TIMEOUT_MS, } from "./shared.js";
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 = 2;
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.appendKernelDiagnostic(`unparseable protocol line: ${line.slice(0, 200)}`);
186
- continue;
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
- if (isRecord(event))
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", ...result } });
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 dispose`);
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; // superseded mid-flush: the newer owner already cleaned this kernel
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 (this.child?.stdin && !this.child.stdin.destroyed) {
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
- /** Best-effort final snapshot before a graceful dispose, bounded by a timeout. */
988
- async flushSnapshotForDispose() {
989
- if (!this.options.snapshot || !this.isRunning)
990
- return;
991
- let timeout;
992
- const guard = new Promise((resolve) => {
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
- try {
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
- /** Best-effort bounded protocol shutdown; the caller's hard kill remains the backstop. */
1040
- async requestProtocolShutdown(timeoutMs) {
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
- const requestId = uuid();
1045
- let timer;
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 doneReply = new Promise((resolve) => {
1048
- this.pendingDoneWaiters.set(requestId, resolve);
1049
- });
1050
- const send = this.writeLine({ type: "shutdown", id: requestId });
1051
- send.catch(() => undefined);
1052
- const kernelExit = this.waitForKernelExit();
1053
- const deadline = new Promise((resolve) => {
1054
- timer = globalThis.setTimeout(resolve, timeoutMs);
1055
- timer.unref?.();
1056
- });
1057
- const gracefulReply = Promise.all([send, doneReply]).then(() => undefined);
1058
- gracefulReply.catch(() => undefined);
1059
- await Promise.race([gracefulReply, kernelExit, deadline]);
1060
- await Promise.race([kernelExit, deadline]);
1061
- }
1062
- catch {
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
- if (timer)
1067
- globalThis.clearTimeout(timer);
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();