@code-yeongyu/senpi 2026.9.5 → 2026.9.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.
Files changed (150) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/core/agent-session.d.ts +9 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +95 -31
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/compaction/compaction.d.ts +6 -2
  7. package/dist/core/compaction/compaction.d.ts.map +1 -1
  8. package/dist/core/compaction/compaction.js +4 -0
  9. package/dist/core/compaction/compaction.js.map +1 -1
  10. package/dist/core/extensions/builtin/claude-sdk-oauth/prompt-bridge.d.ts.map +1 -1
  11. package/dist/core/extensions/builtin/claude-sdk-oauth/prompt-bridge.js +2 -0
  12. package/dist/core/extensions/builtin/claude-sdk-oauth/prompt-bridge.js.map +1 -1
  13. package/dist/core/extensions/builtin/claude-sdk-oauth/session-registry-pump.d.ts.map +1 -1
  14. package/dist/core/extensions/builtin/claude-sdk-oauth/session-registry-pump.js +15 -2
  15. package/dist/core/extensions/builtin/claude-sdk-oauth/session-registry-pump.js.map +1 -1
  16. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-claim.d.ts +1 -1
  17. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-claim.d.ts.map +1 -1
  18. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-claim.js +2 -3
  19. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-claim.js.map +1 -1
  20. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts +4 -0
  21. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts.map +1 -1
  22. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +16 -19
  23. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js.map +1 -1
  24. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +9 -0
  25. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts.map +1 -1
  26. package/dist/core/extensions/builtin/compaction/lane-policy.js +5 -0
  27. package/dist/core/extensions/builtin/compaction/lane-policy.js.map +1 -1
  28. package/dist/core/extensions/builtin/compaction/openai-remote-convert.d.ts.map +1 -1
  29. package/dist/core/extensions/builtin/compaction/openai-remote-convert.js +2 -0
  30. package/dist/core/extensions/builtin/compaction/openai-remote-convert.js.map +1 -1
  31. package/dist/core/extensions/builtin/compaction/retained-message-safety.d.ts.map +1 -1
  32. package/dist/core/extensions/builtin/compaction/retained-message-safety.js +14 -1
  33. package/dist/core/extensions/builtin/compaction/retained-message-safety.js.map +1 -1
  34. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts +2 -2
  35. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts.map +1 -1
  36. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +8 -2
  37. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js.map +1 -1
  38. package/dist/core/messages.d.ts +7 -0
  39. package/dist/core/messages.d.ts.map +1 -1
  40. package/dist/core/messages.js +2 -0
  41. package/dist/core/messages.js.map +1 -1
  42. package/dist/core/retry-fallback/controller.d.ts.map +1 -1
  43. package/dist/core/retry-fallback/controller.js +5 -4
  44. package/dist/core/retry-fallback/controller.js.map +1 -1
  45. package/dist/core/sdk.d.ts.map +1 -1
  46. package/dist/core/sdk.js +5 -0
  47. package/dist/core/sdk.js.map +1 -1
  48. package/dist/core/session-manager.d.ts +12 -1
  49. package/dist/core/session-manager.d.ts.map +1 -1
  50. package/dist/core/session-manager.js +29 -3
  51. package/dist/core/session-manager.js.map +1 -1
  52. package/dist/core/settings-manager.d.ts +6 -2
  53. package/dist/core/settings-manager.d.ts.map +1 -1
  54. package/dist/core/settings-manager.js +4 -1
  55. package/dist/core/settings-manager.js.map +1 -1
  56. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  57. package/dist/modes/interactive/interactive-mode.js +4 -1
  58. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  59. package/dist/modes/rpc/multi-session-host.d.ts +3 -7
  60. package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
  61. package/dist/modes/rpc/multi-session-host.js +3 -13
  62. package/dist/modes/rpc/multi-session-host.js.map +1 -1
  63. package/dist/modes/rpc/rpc-types.d.ts +1 -2
  64. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  65. package/dist/modes/rpc/rpc-types.js +0 -1
  66. package/dist/modes/rpc/rpc-types.js.map +1 -1
  67. package/dist/modes/rpc/session-registry.d.ts +1 -4
  68. package/dist/modes/rpc/session-registry.d.ts.map +1 -1
  69. package/dist/modes/rpc/session-registry.js +0 -15
  70. package/dist/modes/rpc/session-registry.js.map +1 -1
  71. package/docs/rpc.md +5 -6
  72. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +45 -0
  73. package/node_modules/@code-yeongyu/senpi-codemode/README.md +20 -4
  74. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  75. package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/reserved.ts +2 -0
  76. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +73 -129
  77. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/interrupt-bounds.ts +66 -0
  78. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/run-queue.ts +18 -3
  79. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-core.js +30 -0
  80. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-runtime.js +16 -0
  81. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-shell-capture.d.ts +9 -0
  82. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-shell-capture.js +32 -6
  83. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-slot.ts +106 -0
  84. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-startup.ts +69 -0
  85. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +10 -5
  86. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-execution.ts +9 -11
  87. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-contract.ts +45 -0
  88. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-manager.ts +43 -71
  89. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-notification.ts +4 -5
  90. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-snapshot.ts +2 -0
  91. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-status.ts +30 -0
  92. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-eval-result.ts +1 -0
  93. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-notification-queue.ts +2 -2
  94. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/interrupt-note.ts +29 -15
  95. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +2 -0
  96. package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts.map +1 -1
  97. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +3 -1
  98. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js.map +1 -1
  99. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
  100. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js +17 -0
  101. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
  102. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.d.ts.map +1 -1
  103. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js +1 -0
  104. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js.map +1 -1
  105. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.d.ts.map +1 -1
  106. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +1 -0
  107. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js.map +1 -1
  108. package/node_modules/@earendil-works/pi-agent-core/dist/harness/reducer.d.ts.map +1 -1
  109. package/node_modules/@earendil-works/pi-agent-core/dist/harness/reducer.js +6 -0
  110. package/node_modules/@earendil-works/pi-agent-core/dist/harness/reducer.js.map +1 -1
  111. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/context.d.ts +2 -1
  112. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/context.d.ts.map +1 -1
  113. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/context.js +5 -1
  114. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/context.js.map +1 -1
  115. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl/codec.d.ts.map +1 -1
  116. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl/codec.js +1 -0
  117. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl/codec.js.map +1 -1
  118. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/types.d.ts +7 -1
  119. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/types.d.ts.map +1 -1
  120. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/types.js.map +1 -1
  121. package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts +2 -0
  122. package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts.map +1 -1
  123. package/node_modules/@earendil-works/pi-agent-core/dist/types.js.map +1 -1
  124. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  125. package/node_modules/@earendil-works/pi-ai/dist/api/mistral-conversations.js +2 -0
  126. package/node_modules/@earendil-works/pi-ai/dist/api/mistral-conversations.js.map +1 -1
  127. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.d.ts.map +1 -1
  128. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js +15 -0
  129. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js.map +1 -1
  130. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +4 -4
  131. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js.map +1 -1
  132. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  133. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cerebras.json +1 -1
  134. package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai-cn.json +1 -1
  135. package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai.json +1 -1
  136. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  137. package/node_modules/@earendil-works/pi-ai/dist/providers/faux.d.ts.map +1 -1
  138. package/node_modules/@earendil-works/pi-ai/dist/providers/faux.js +2 -0
  139. package/node_modules/@earendil-works/pi-ai/dist/providers/faux.js.map +1 -1
  140. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +8 -1
  141. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
  142. package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
  143. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.d.ts.map +1 -1
  144. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.js +2 -0
  145. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.js.map +1 -1
  146. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  147. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  148. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  149. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  150. package/package.json +7 -7
@@ -68,13 +68,6 @@ export class RpcSessionRegistry {
68
68
  attached: true,
69
69
  };
70
70
  }
71
- const maxSessions = this.options.maxSessions;
72
- if (maxSessions !== undefined && this.countActiveSessions() >= maxSessions) {
73
- // Reconnect-churn backstop: every open_session owns a complete runtime
74
- // (hundreds of MB measured), so admission is bounded. Attachments to a
75
- // live session add none and stay allowed via the branch above.
76
- throw new RpcSessionRegistryError("too_many_sessions");
77
- }
78
71
  if (sessionPath)
79
72
  this.reservations.add(sessionPath);
80
73
  // Resume vs create parity (D1 + omo SenpiSessionRuntime.ts:198-200):
@@ -238,13 +231,5 @@ export class RpcSessionRegistry {
238
231
  throw new RpcSessionRegistryError("invalid_path");
239
232
  }
240
233
  }
241
- countActiveSessions() {
242
- let count = 0;
243
- for (const entry of this.entries.values()) {
244
- if (entry.state === "opening" || entry.state === "open")
245
- count += 1;
246
- }
247
- return count;
248
- }
249
234
  }
250
235
  //# sourceMappingURL=session-registry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-registry.js","sourceRoot":"","sources":["../../../src/modes/rpc/session-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, realpathSync } from \"node:fs\";\nimport { basename, dirname, isAbsolute, resolve } from \"node:path\";\nimport { ProviderScope, runWithProviderScope } from \"@earendil-works/pi-ai/node/provider-scope\";\nimport {\n\ttype AgentSessionLaunchProfile,\n\tAgentSessionRuntime,\n\ttype CreateAgentSessionRuntimeFactory,\n\tcreateAgentSessionRuntime,\n} from \"../../core/agent-session-runtime.ts\";\nimport { SessionManager } from \"../../core/session-manager.ts\";\nimport { beginSessionClose, closeMarkedSession, closeSession, type SessionTeardownHost } from \"./session-teardown.ts\";\n\n/** The immutable flags selected when a routing session is opened. */\nexport interface RpcSessionLaunchProfile extends AgentSessionLaunchProfile {\n\tsessionPath?: string;\n}\n\nexport type SessionRuntime = AgentSessionRuntime;\nexport type RpcSessionState = \"opening\" | \"open\" | \"closing\" | \"closed\";\n\nexport interface RpcSessionEntry {\n\tstate: RpcSessionState;\n\truntime?: SessionRuntime;\n\t/** Resolves replacement against the runtime currently owned by this entry. */\n\tswitchSession?: SessionRuntime[\"switchSession\"];\n\t/** Rebind callback installed by the shared RPC connection handler. */\n\trebindSession?: Parameters<SessionRuntime[\"setRebindSession\"]>[0];\n\tscope: ProviderScope;\n\tprofile: Readonly<RpcSessionLaunchProfile>;\n\tdurableSessionId?: string;\n\tsessionPath?: string;\n\t/** Canonical reservation key for path-opened sessions; matches the reservations set. */\n\treservationKey?: string;\n\t/** Current runtime cwd, which can change when a session is replaced. */\n\tcwd: string;\n\t/** Live attachments (open + later attaches). The runtime is disposed only when the last one closes. */\n\tattachments: number;\n\t/** Timestamp of the last routed command / observed activity; drives idle eviction. */\n\tlastCommandAt: number;\n\tlifecycleMutex: Promise<void>;\n\tcloseCompletion?: Promise<void>;\n\tcloseResolve?: () => void;\n\tcloseStarted?: boolean;\n}\n\nexport class RpcSessionRegistryError extends Error {\n\treadonly code:\n\t\t| \"unknown_session\"\n\t\t| \"session_closing\"\n\t\t| \"session_path_in_use\"\n\t\t| \"invalid_path\"\n\t\t| \"open_failed\"\n\t\t| \"too_many_sessions\";\n\n\tconstructor(code: RpcSessionRegistryError[\"code\"], reason?: string) {\n\t\tsuper(code === \"open_failed\" && reason ? `${code}: ${reason}` : code);\n\t\tthis.code = code;\n\t\tthis.name = \"RpcSessionRegistryError\";\n\t}\n}\n\nexport interface RpcSessionRegistryOptions {\n\tagentDir: string;\n\tcreateRuntime: CreateAgentSessionRuntimeFactory;\n\t/** Injectable clock (defaults to Date.now) so idle bookkeeping is testable. */\n\tnow?: () => number;\n\t/** Cap on concurrently opening/open sessions; attach-on-open is exempt. */\n\tmaxSessions?: number;\n\t/** Maximum time to wait for graceful runtime teardown before forced release. */\n\tcloseGraceMs?: number;\n}\n\nexport interface OpenRpcSession {\n\tsessionId: string;\n\tdurableSessionId: string;\n\tsessionPath?: string;\n\t/** True when this open attached to an already-open session instead of creating one. */\n\tattached?: boolean;\n}\n\nfunction canonicalPath(path: string): string {\n\tconst absolutePath = resolve(path);\n\tif (existsSync(absolutePath)) return realpathSync(absolutePath);\n\treturn `${realpathSync(dirname(absolutePath))}/${basename(absolutePath)}`;\n}\n\nfunction frozenProfile(profile: RpcSessionLaunchProfile): Readonly<RpcSessionLaunchProfile> {\n\treturn Object.freeze({\n\t\t...profile,\n\t\t...(profile.creationModel ? { creationModel: Object.freeze({ ...profile.creationModel }) } : {}),\n\t});\n}\n\n/** Process-local lifecycle owner for multi-session RPC runtimes. */\nexport class RpcSessionRegistry {\n\tprivate readonly entries = new Map<string, RpcSessionEntry>();\n\tprivate readonly reservations = new Set<string>();\n\tprivate readonly teardownHost: SessionTeardownHost;\n\tprivate nextHandle = 0;\n\tprivate readonly options: RpcSessionRegistryOptions;\n\tprivate readonly now: () => number;\n\treadonly closeGraceMs: number;\n\n\tconstructor(options: RpcSessionRegistryOptions) {\n\t\tthis.options = options;\n\t\tthis.now = options.now ?? Date.now;\n\t\tthis.closeGraceMs = options.closeGraceMs ?? 10_000;\n\t\tthis.teardownHost = {\n\t\t\tcloseGraceMs: this.closeGraceMs,\n\t\t\tget: (handle) => this.entries.get(handle),\n\t\t\tdelete: (handle) => this.entries.delete(handle),\n\t\t\treleaseReservation: (key) => this.reservations.delete(key),\n\t\t\tsync: () => this.syncRuntimeMetadata(),\n\t\t};\n\t}\n\n\t/** Number of live entries, including ones still opening or closing. */\n\tget size(): number {\n\t\treturn this.entries.size;\n\t}\n\n\tasync openSession(profile: RpcSessionLaunchProfile): Promise<OpenRpcSession> {\n\t\tthis.validateProfile(profile);\n\t\tthis.syncRuntimeMetadata();\n\t\tconst sessionPath = profile.sessionPath ? canonicalPath(profile.sessionPath) : undefined;\n\t\tif (sessionPath && this.reservations.has(sessionPath)) {\n\t\t\t// Attach-on-open: a live session outlives individual client attachments, so a\n\t\t\t// resume (or a second surface) for an already-hosted path joins the existing\n\t\t\t// runtime instead of failing. Entries still opening or closing keep the\n\t\t\t// exclusive reservation and reject as before.\n\t\t\tconst existing = [...this.entries].find(\n\t\t\t\t([, entry]) => entry.reservationKey === sessionPath && entry.state === \"open\",\n\t\t\t);\n\t\t\tif (!existing) throw new RpcSessionRegistryError(\"session_path_in_use\");\n\t\t\tconst [handle, entry] = existing;\n\t\t\tentry.attachments += 1;\n\t\t\tif (!entry.durableSessionId) throw new RpcSessionRegistryError(\"session_path_in_use\");\n\t\t\tentry.lastCommandAt = this.now();\n\t\t\treturn {\n\t\t\t\tsessionId: handle,\n\t\t\t\tdurableSessionId: entry.durableSessionId,\n\t\t\t\tsessionPath: entry.sessionPath,\n\t\t\t\tattached: true,\n\t\t\t};\n\t\t}\n\t\tconst maxSessions = this.options.maxSessions;\n\t\tif (maxSessions !== undefined && this.countActiveSessions() >= maxSessions) {\n\t\t\t// Reconnect-churn backstop: every open_session owns a complete runtime\n\t\t\t// (hundreds of MB measured), so admission is bounded. Attachments to a\n\t\t\t// live session add none and stay allowed via the branch above.\n\t\t\tthrow new RpcSessionRegistryError(\"too_many_sessions\");\n\t\t}\n\t\tif (sessionPath) this.reservations.add(sessionPath);\n\n\t\t// Resume vs create parity (D1 + omo SenpiSessionRuntime.ts:198-200):\n\t\t// Create-only launch semantics mirror classic startup flags. A resumed\n\t\t// session restores its persisted model and thinking level instead of being\n\t\t// overridden by the new open_session request.\n\t\tconst isResume = sessionPath !== undefined && existsSync(sessionPath);\n\t\tconst storedProfile = frozenProfile({ ...profile, ...(sessionPath ? { sessionPath } : {}) });\n\t\tconst runtimeProfile = isResume\n\t\t\t? frozenProfile({ ...storedProfile, creationModel: undefined, initialThinkingLevel: undefined })\n\t\t\t: storedProfile;\n\n\t\tconst handle = `rpc-${++this.nextHandle}`;\n\t\tconst entry: RpcSessionEntry = {\n\t\t\tstate: \"opening\",\n\t\t\tscope: new ProviderScope(),\n\t\t\tprofile: storedProfile,\n\t\t\tsessionPath,\n\t\t\treservationKey: sessionPath,\n\t\t\tcwd: storedProfile.cwd,\n\t\t\tattachments: 1,\n\t\t\tlastCommandAt: this.now(),\n\t\t\tlifecycleMutex: Promise.resolve(),\n\t\t};\n\t\tentry.switchSession = (sessionPath, options) => {\n\t\t\tconst operation = entry.lifecycleMutex.then(async () => {\n\t\t\t\tif (entry.state !== \"open\" || !entry.runtime) {\n\t\t\t\t\tthrow new RpcSessionRegistryError(\"unknown_session\");\n\t\t\t\t}\n\t\t\t\tconst runtime = entry.runtime;\n\t\t\t\tconst cwdOverride = options?.cwdOverride;\n\t\t\t\tconst cwdChanged = cwdOverride !== undefined && runtime.session.sessionManager.getCwd() !== cwdOverride;\n\t\t\t\tconst result = await runtime.switchSession(sessionPath, options);\n\t\t\t\tif (result.cancelled || !cwdChanged) return result;\n\n\t\t\t\t// A multi-session binding outlives an individual replacement. Keep the\n\t\t\t\t// entry's runtime object aligned with the replacement so every attached\n\t\t\t\t// client resolves getters and future commands against the new cwd-bound\n\t\t\t\t// runtime, not the object created during open_session.\n\t\t\t\tconst replacement = new AgentSessionRuntime(\n\t\t\t\t\truntime.session,\n\t\t\t\t\truntime.services,\n\t\t\t\t\tthis.options.createRuntime,\n\t\t\t\t\t[...runtime.diagnostics],\n\t\t\t\t\truntime.modelFallbackMessage,\n\t\t\t\t\truntime.launchProfile,\n\t\t\t\t);\n\t\t\t\treplacement.setRebindSession(entry.rebindSession);\n\t\t\t\tentry.runtime = replacement;\n\t\t\t\tthis.syncRuntimeMetadata();\n\t\t\t\treturn result;\n\t\t\t});\n\t\t\tentry.lifecycleMutex = operation.then(\n\t\t\t\t() => undefined,\n\t\t\t\t() => undefined,\n\t\t\t);\n\t\t\treturn operation;\n\t\t};\n\t\tthis.entries.set(handle, entry);\n\t\ttry {\n\t\t\tconst manager = sessionPath\n\t\t\t\t? SessionManager.open(sessionPath, undefined, storedProfile.cwd)\n\t\t\t\t: SessionManager.create(storedProfile.cwd);\n\t\t\tentry.runtime = await runWithProviderScope(entry.scope, () =>\n\t\t\t\tcreateAgentSessionRuntime(this.options.createRuntime, {\n\t\t\t\t\tcwd: manager.getCwd(),\n\t\t\t\t\tagentDir: this.options.agentDir,\n\t\t\t\t\tsessionManager: manager,\n\t\t\t\t\tlaunchProfile: runtimeProfile,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tentry.durableSessionId = manager.getSessionId();\n\t\t\tentry.sessionPath ??= manager.getSessionFile();\n\t\t\tentry.state = \"open\";\n\t\t\treturn { sessionId: handle, durableSessionId: entry.durableSessionId, sessionPath: entry.sessionPath };\n\t\t} catch (error) {\n\t\t\t// Runtime construction may have started extensions, watchers, and provider\n\t\t\t// registrations before it rejects. Keep the reservation and entry private\n\t\t\t// until all of those resources have been torn down, then release them as\n\t\t\t// one rollback so the path can immediately be opened again.\n\t\t\ttry {\n\t\t\t\tawait entry.runtime?.dispose();\n\t\t\t} catch {\n\t\t\t\t// The original construction error remains the externally visible cause.\n\t\t\t} finally {\n\t\t\t\ttry {\n\t\t\t\t\tawait entry.scope.close?.();\n\t\t\t\t} finally {\n\t\t\t\t\tthis.entries.delete(handle);\n\t\t\t\t\tif (sessionPath) this.reservations.delete(sessionPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (error instanceof RpcSessionRegistryError) throw error;\n\t\t\tthrow new RpcSessionRegistryError(\"open_failed\", error instanceof Error ? error.message : undefined);\n\t\t}\n\t}\n\n\t/**\n\t * Read-only lookup with no state transitions or attachment accounting.\n\t * Exists so lifecycle decisions (e.g. deferring a dropped connection's\n\t * release while a turn is still streaming) can inspect the live entry\n\t * without claiming it.\n\t */\n\tpeek(handle: string): RpcSessionEntry | undefined {\n\t\treturn this.entries.get(handle);\n\t}\n\n\tgetForCommand(handle: string, command: string): RpcSessionEntry {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (!entry) throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tif (entry.state === \"closing\" && ![\"abort\", \"abort_bash\", \"extension_ui_response\"].includes(command)) {\n\t\t\tthrow new RpcSessionRegistryError(\"session_closing\");\n\t\t}\n\t\tif (entry.state !== \"open\" && entry.state !== \"closing\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\t// Every routed command counts as activity for idle eviction. Lookups that\n\t\t// must not refresh idleness (sweeps, listing) use peek()/list() instead.\n\t\tentry.lastCommandAt = this.now();\n\t\treturn entry;\n\t}\n\n\t/** Starts a close synchronously and returns the live entry for routing decisions. */\n\tbeginClose(handle: string, onRole?: (finalizer: boolean) => void): RpcSessionEntry {\n\t\treturn beginSessionClose(this.teardownHost, handle, onRole);\n\t}\n\n\tasync close(handle: string): Promise<void> {\n\t\treturn closeSession(this.teardownHost, handle);\n\t}\n\n\t/** Completes a close previously made visible by beginClose(). */\n\tasync closeMarked(handle: string): Promise<void> {\n\t\treturn closeMarkedSession(this.teardownHost, handle);\n\t}\n\n\tlist(): Array<{\n\t\tsessionId: string;\n\t\tdurableSessionId?: string;\n\t\tsessionPath?: string;\n\t\tcwd: string;\n\t\tname?: string;\n\t\tstatus: RpcSessionState;\n\t}> {\n\t\tthis.syncRuntimeMetadata();\n\t\treturn [...this.entries].map(([sessionId, entry]) => ({\n\t\t\tsessionId,\n\t\t\tdurableSessionId: entry.durableSessionId,\n\t\t\tsessionPath: entry.sessionPath,\n\t\t\tcwd: entry.cwd,\n\t\t\tname: entry.runtime?.session.sessionManager.getSessionName(),\n\t\t\tstatus: entry.state,\n\t\t}));\n\t}\n\n\t/** Reconcile path and durable identity after runtime replacement. */\n\tprivate syncRuntimeMetadata(): void {\n\t\tfor (const entry of this.entries.values()) {\n\t\t\tconst manager = entry.runtime?.session.sessionManager;\n\t\t\tif (!manager) continue;\n\t\t\tconst currentPath = manager.getSessionFile();\n\t\t\t// Preserve the originally canonicalized key while the runtime still points at\n\t\t\t// the same path. SessionManager may expose a symlink-resolved spelling after\n\t\t\t// opening a file that did not exist yet; treating that as replacement would\n\t\t\t// break ordinary attach-on-open aliases.\n\t\t\tif (currentPath !== entry.sessionPath) {\n\t\t\t\tconst currentKey = currentPath ? canonicalPath(currentPath) : undefined;\n\t\t\t\tif (entry.reservationKey) this.reservations.delete(entry.reservationKey);\n\t\t\t\tif (currentKey) this.reservations.add(currentKey);\n\t\t\t\tentry.reservationKey = currentKey;\n\t\t\t\tentry.sessionPath = currentPath;\n\t\t\t}\n\t\t\tentry.durableSessionId = manager.getSessionId();\n\t\t\tentry.cwd = manager.getCwd();\n\t\t}\n\t}\n\n\tprivate validateProfile(profile: RpcSessionLaunchProfile): void {\n\t\tif (!isAbsolute(profile.cwd) || (profile.sessionPath !== undefined && !isAbsolute(profile.sessionPath))) {\n\t\t\tthrow new RpcSessionRegistryError(\"invalid_path\");\n\t\t}\n\t}\n\n\tprivate countActiveSessions(): number {\n\t\tlet count = 0;\n\t\tfor (const entry of this.entries.values()) {\n\t\t\tif (entry.state === \"opening\" || entry.state === \"open\") count += 1;\n\t\t}\n\t\treturn count;\n\t}\n}\n"]}
1
+ {"version":3,"file":"session-registry.js","sourceRoot":"","sources":["../../../src/modes/rpc/session-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, realpathSync } from \"node:fs\";\nimport { basename, dirname, isAbsolute, resolve } from \"node:path\";\nimport { ProviderScope, runWithProviderScope } from \"@earendil-works/pi-ai/node/provider-scope\";\nimport {\n\ttype AgentSessionLaunchProfile,\n\tAgentSessionRuntime,\n\ttype CreateAgentSessionRuntimeFactory,\n\tcreateAgentSessionRuntime,\n} from \"../../core/agent-session-runtime.ts\";\nimport { SessionManager } from \"../../core/session-manager.ts\";\nimport { beginSessionClose, closeMarkedSession, closeSession, type SessionTeardownHost } from \"./session-teardown.ts\";\n\n/** The immutable flags selected when a routing session is opened. */\nexport interface RpcSessionLaunchProfile extends AgentSessionLaunchProfile {\n\tsessionPath?: string;\n}\n\nexport type SessionRuntime = AgentSessionRuntime;\nexport type RpcSessionState = \"opening\" | \"open\" | \"closing\" | \"closed\";\n\nexport interface RpcSessionEntry {\n\tstate: RpcSessionState;\n\truntime?: SessionRuntime;\n\t/** Resolves replacement against the runtime currently owned by this entry. */\n\tswitchSession?: SessionRuntime[\"switchSession\"];\n\t/** Rebind callback installed by the shared RPC connection handler. */\n\trebindSession?: Parameters<SessionRuntime[\"setRebindSession\"]>[0];\n\tscope: ProviderScope;\n\tprofile: Readonly<RpcSessionLaunchProfile>;\n\tdurableSessionId?: string;\n\tsessionPath?: string;\n\t/** Canonical reservation key for path-opened sessions; matches the reservations set. */\n\treservationKey?: string;\n\t/** Current runtime cwd, which can change when a session is replaced. */\n\tcwd: string;\n\t/** Live attachments (open + later attaches). The runtime is disposed only when the last one closes. */\n\tattachments: number;\n\t/** Timestamp of the last routed command / observed activity; drives idle eviction. */\n\tlastCommandAt: number;\n\tlifecycleMutex: Promise<void>;\n\tcloseCompletion?: Promise<void>;\n\tcloseResolve?: () => void;\n\tcloseStarted?: boolean;\n}\n\nexport class RpcSessionRegistryError extends Error {\n\treadonly code: \"unknown_session\" | \"session_closing\" | \"session_path_in_use\" | \"invalid_path\" | \"open_failed\";\n\n\tconstructor(code: RpcSessionRegistryError[\"code\"], reason?: string) {\n\t\tsuper(code === \"open_failed\" && reason ? `${code}: ${reason}` : code);\n\t\tthis.code = code;\n\t\tthis.name = \"RpcSessionRegistryError\";\n\t}\n}\n\nexport interface RpcSessionRegistryOptions {\n\tagentDir: string;\n\tcreateRuntime: CreateAgentSessionRuntimeFactory;\n\t/** Injectable clock (defaults to Date.now) so idle bookkeeping is testable. */\n\tnow?: () => number;\n\t/** Maximum time to wait for graceful runtime teardown before forced release. */\n\tcloseGraceMs?: number;\n}\n\nexport interface OpenRpcSession {\n\tsessionId: string;\n\tdurableSessionId: string;\n\tsessionPath?: string;\n\t/** True when this open attached to an already-open session instead of creating one. */\n\tattached?: boolean;\n}\n\nfunction canonicalPath(path: string): string {\n\tconst absolutePath = resolve(path);\n\tif (existsSync(absolutePath)) return realpathSync(absolutePath);\n\treturn `${realpathSync(dirname(absolutePath))}/${basename(absolutePath)}`;\n}\n\nfunction frozenProfile(profile: RpcSessionLaunchProfile): Readonly<RpcSessionLaunchProfile> {\n\treturn Object.freeze({\n\t\t...profile,\n\t\t...(profile.creationModel ? { creationModel: Object.freeze({ ...profile.creationModel }) } : {}),\n\t});\n}\n\n/** Process-local lifecycle owner for multi-session RPC runtimes. */\nexport class RpcSessionRegistry {\n\tprivate readonly entries = new Map<string, RpcSessionEntry>();\n\tprivate readonly reservations = new Set<string>();\n\tprivate readonly teardownHost: SessionTeardownHost;\n\tprivate nextHandle = 0;\n\tprivate readonly options: RpcSessionRegistryOptions;\n\tprivate readonly now: () => number;\n\treadonly closeGraceMs: number;\n\n\tconstructor(options: RpcSessionRegistryOptions) {\n\t\tthis.options = options;\n\t\tthis.now = options.now ?? Date.now;\n\t\tthis.closeGraceMs = options.closeGraceMs ?? 10_000;\n\t\tthis.teardownHost = {\n\t\t\tcloseGraceMs: this.closeGraceMs,\n\t\t\tget: (handle) => this.entries.get(handle),\n\t\t\tdelete: (handle) => this.entries.delete(handle),\n\t\t\treleaseReservation: (key) => this.reservations.delete(key),\n\t\t\tsync: () => this.syncRuntimeMetadata(),\n\t\t};\n\t}\n\n\t/** Number of live entries, including ones still opening or closing. */\n\tget size(): number {\n\t\treturn this.entries.size;\n\t}\n\n\tasync openSession(profile: RpcSessionLaunchProfile): Promise<OpenRpcSession> {\n\t\tthis.validateProfile(profile);\n\t\tthis.syncRuntimeMetadata();\n\t\tconst sessionPath = profile.sessionPath ? canonicalPath(profile.sessionPath) : undefined;\n\t\tif (sessionPath && this.reservations.has(sessionPath)) {\n\t\t\t// Attach-on-open: a live session outlives individual client attachments, so a\n\t\t\t// resume (or a second surface) for an already-hosted path joins the existing\n\t\t\t// runtime instead of failing. Entries still opening or closing keep the\n\t\t\t// exclusive reservation and reject as before.\n\t\t\tconst existing = [...this.entries].find(\n\t\t\t\t([, entry]) => entry.reservationKey === sessionPath && entry.state === \"open\",\n\t\t\t);\n\t\t\tif (!existing) throw new RpcSessionRegistryError(\"session_path_in_use\");\n\t\t\tconst [handle, entry] = existing;\n\t\t\tentry.attachments += 1;\n\t\t\tif (!entry.durableSessionId) throw new RpcSessionRegistryError(\"session_path_in_use\");\n\t\t\tentry.lastCommandAt = this.now();\n\t\t\treturn {\n\t\t\t\tsessionId: handle,\n\t\t\t\tdurableSessionId: entry.durableSessionId,\n\t\t\t\tsessionPath: entry.sessionPath,\n\t\t\t\tattached: true,\n\t\t\t};\n\t\t}\n\t\tif (sessionPath) this.reservations.add(sessionPath);\n\n\t\t// Resume vs create parity (D1 + omo SenpiSessionRuntime.ts:198-200):\n\t\t// Create-only launch semantics mirror classic startup flags. A resumed\n\t\t// session restores its persisted model and thinking level instead of being\n\t\t// overridden by the new open_session request.\n\t\tconst isResume = sessionPath !== undefined && existsSync(sessionPath);\n\t\tconst storedProfile = frozenProfile({ ...profile, ...(sessionPath ? { sessionPath } : {}) });\n\t\tconst runtimeProfile = isResume\n\t\t\t? frozenProfile({ ...storedProfile, creationModel: undefined, initialThinkingLevel: undefined })\n\t\t\t: storedProfile;\n\n\t\tconst handle = `rpc-${++this.nextHandle}`;\n\t\tconst entry: RpcSessionEntry = {\n\t\t\tstate: \"opening\",\n\t\t\tscope: new ProviderScope(),\n\t\t\tprofile: storedProfile,\n\t\t\tsessionPath,\n\t\t\treservationKey: sessionPath,\n\t\t\tcwd: storedProfile.cwd,\n\t\t\tattachments: 1,\n\t\t\tlastCommandAt: this.now(),\n\t\t\tlifecycleMutex: Promise.resolve(),\n\t\t};\n\t\tentry.switchSession = (sessionPath, options) => {\n\t\t\tconst operation = entry.lifecycleMutex.then(async () => {\n\t\t\t\tif (entry.state !== \"open\" || !entry.runtime) {\n\t\t\t\t\tthrow new RpcSessionRegistryError(\"unknown_session\");\n\t\t\t\t}\n\t\t\t\tconst runtime = entry.runtime;\n\t\t\t\tconst cwdOverride = options?.cwdOverride;\n\t\t\t\tconst cwdChanged = cwdOverride !== undefined && runtime.session.sessionManager.getCwd() !== cwdOverride;\n\t\t\t\tconst result = await runtime.switchSession(sessionPath, options);\n\t\t\t\tif (result.cancelled || !cwdChanged) return result;\n\n\t\t\t\t// A multi-session binding outlives an individual replacement. Keep the\n\t\t\t\t// entry's runtime object aligned with the replacement so every attached\n\t\t\t\t// client resolves getters and future commands against the new cwd-bound\n\t\t\t\t// runtime, not the object created during open_session.\n\t\t\t\tconst replacement = new AgentSessionRuntime(\n\t\t\t\t\truntime.session,\n\t\t\t\t\truntime.services,\n\t\t\t\t\tthis.options.createRuntime,\n\t\t\t\t\t[...runtime.diagnostics],\n\t\t\t\t\truntime.modelFallbackMessage,\n\t\t\t\t\truntime.launchProfile,\n\t\t\t\t);\n\t\t\t\treplacement.setRebindSession(entry.rebindSession);\n\t\t\t\tentry.runtime = replacement;\n\t\t\t\tthis.syncRuntimeMetadata();\n\t\t\t\treturn result;\n\t\t\t});\n\t\t\tentry.lifecycleMutex = operation.then(\n\t\t\t\t() => undefined,\n\t\t\t\t() => undefined,\n\t\t\t);\n\t\t\treturn operation;\n\t\t};\n\t\tthis.entries.set(handle, entry);\n\t\ttry {\n\t\t\tconst manager = sessionPath\n\t\t\t\t? SessionManager.open(sessionPath, undefined, storedProfile.cwd)\n\t\t\t\t: SessionManager.create(storedProfile.cwd);\n\t\t\tentry.runtime = await runWithProviderScope(entry.scope, () =>\n\t\t\t\tcreateAgentSessionRuntime(this.options.createRuntime, {\n\t\t\t\t\tcwd: manager.getCwd(),\n\t\t\t\t\tagentDir: this.options.agentDir,\n\t\t\t\t\tsessionManager: manager,\n\t\t\t\t\tlaunchProfile: runtimeProfile,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tentry.durableSessionId = manager.getSessionId();\n\t\t\tentry.sessionPath ??= manager.getSessionFile();\n\t\t\tentry.state = \"open\";\n\t\t\treturn { sessionId: handle, durableSessionId: entry.durableSessionId, sessionPath: entry.sessionPath };\n\t\t} catch (error) {\n\t\t\t// Runtime construction may have started extensions, watchers, and provider\n\t\t\t// registrations before it rejects. Keep the reservation and entry private\n\t\t\t// until all of those resources have been torn down, then release them as\n\t\t\t// one rollback so the path can immediately be opened again.\n\t\t\ttry {\n\t\t\t\tawait entry.runtime?.dispose();\n\t\t\t} catch {\n\t\t\t\t// The original construction error remains the externally visible cause.\n\t\t\t} finally {\n\t\t\t\ttry {\n\t\t\t\t\tawait entry.scope.close?.();\n\t\t\t\t} finally {\n\t\t\t\t\tthis.entries.delete(handle);\n\t\t\t\t\tif (sessionPath) this.reservations.delete(sessionPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (error instanceof RpcSessionRegistryError) throw error;\n\t\t\tthrow new RpcSessionRegistryError(\"open_failed\", error instanceof Error ? error.message : undefined);\n\t\t}\n\t}\n\n\t/**\n\t * Read-only lookup with no state transitions or attachment accounting.\n\t * Exists so lifecycle decisions (e.g. deferring a dropped connection's\n\t * release while a turn is still streaming) can inspect the live entry\n\t * without claiming it.\n\t */\n\tpeek(handle: string): RpcSessionEntry | undefined {\n\t\treturn this.entries.get(handle);\n\t}\n\n\tgetForCommand(handle: string, command: string): RpcSessionEntry {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (!entry) throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tif (entry.state === \"closing\" && ![\"abort\", \"abort_bash\", \"extension_ui_response\"].includes(command)) {\n\t\t\tthrow new RpcSessionRegistryError(\"session_closing\");\n\t\t}\n\t\tif (entry.state !== \"open\" && entry.state !== \"closing\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\t// Every routed command counts as activity for idle eviction. Lookups that\n\t\t// must not refresh idleness (sweeps, listing) use peek()/list() instead.\n\t\tentry.lastCommandAt = this.now();\n\t\treturn entry;\n\t}\n\n\t/** Starts a close synchronously and returns the live entry for routing decisions. */\n\tbeginClose(handle: string, onRole?: (finalizer: boolean) => void): RpcSessionEntry {\n\t\treturn beginSessionClose(this.teardownHost, handle, onRole);\n\t}\n\n\tasync close(handle: string): Promise<void> {\n\t\treturn closeSession(this.teardownHost, handle);\n\t}\n\n\t/** Completes a close previously made visible by beginClose(). */\n\tasync closeMarked(handle: string): Promise<void> {\n\t\treturn closeMarkedSession(this.teardownHost, handle);\n\t}\n\n\tlist(): Array<{\n\t\tsessionId: string;\n\t\tdurableSessionId?: string;\n\t\tsessionPath?: string;\n\t\tcwd: string;\n\t\tname?: string;\n\t\tstatus: RpcSessionState;\n\t}> {\n\t\tthis.syncRuntimeMetadata();\n\t\treturn [...this.entries].map(([sessionId, entry]) => ({\n\t\t\tsessionId,\n\t\t\tdurableSessionId: entry.durableSessionId,\n\t\t\tsessionPath: entry.sessionPath,\n\t\t\tcwd: entry.cwd,\n\t\t\tname: entry.runtime?.session.sessionManager.getSessionName(),\n\t\t\tstatus: entry.state,\n\t\t}));\n\t}\n\n\t/** Reconcile path and durable identity after runtime replacement. */\n\tprivate syncRuntimeMetadata(): void {\n\t\tfor (const entry of this.entries.values()) {\n\t\t\tconst manager = entry.runtime?.session.sessionManager;\n\t\t\tif (!manager) continue;\n\t\t\tconst currentPath = manager.getSessionFile();\n\t\t\t// Preserve the originally canonicalized key while the runtime still points at\n\t\t\t// the same path. SessionManager may expose a symlink-resolved spelling after\n\t\t\t// opening a file that did not exist yet; treating that as replacement would\n\t\t\t// break ordinary attach-on-open aliases.\n\t\t\tif (currentPath !== entry.sessionPath) {\n\t\t\t\tconst currentKey = currentPath ? canonicalPath(currentPath) : undefined;\n\t\t\t\tif (entry.reservationKey) this.reservations.delete(entry.reservationKey);\n\t\t\t\tif (currentKey) this.reservations.add(currentKey);\n\t\t\t\tentry.reservationKey = currentKey;\n\t\t\t\tentry.sessionPath = currentPath;\n\t\t\t}\n\t\t\tentry.durableSessionId = manager.getSessionId();\n\t\t\tentry.cwd = manager.getCwd();\n\t\t}\n\t}\n\n\tprivate validateProfile(profile: RpcSessionLaunchProfile): void {\n\t\tif (!isAbsolute(profile.cwd) || (profile.sessionPath !== undefined && !isAbsolute(profile.sessionPath))) {\n\t\t\tthrow new RpcSessionRegistryError(\"invalid_path\");\n\t\t}\n\t}\n}\n"]}
package/docs/rpc.md CHANGED
@@ -188,16 +188,16 @@ occupancy bounds itself, independent of the supervisor and of client cooperation
188
188
  background terminal jobs and any other published wake source (terminal monitors, loop-guard holds), compaction,
189
189
  and barrier-held session work all defer eviction, and the idle clock restarts when that work settles. An evicted
190
190
  session resumes like any other: the next `open_session` with the same `sessionPath` reopens it.
191
- - **Session cap**: `open_session` beyond `SENPI_RPC_MAX_SESSIONS` concurrently opening/open sessions (default 8)
192
- fails with `too_many_sessions`. Attaching to an already-hosted session (`attached: true`) adds no runtime and never
193
- counts against the cap, so resume and second-surface flows keep working while at it.
191
+ - **Logical sessions**: `open_session` has no session-count admission limit. Every logical session remains isolated
192
+ by its routing handle and provider scope inside this one shared daemon; idle eviction and empty-host shutdown
193
+ reclaim resident resources without rejecting a new session.
194
194
  - **Empty-host exit**: when the registry holds zero sessions AND no client is connected, continuously for
195
195
  `SENPI_RPC_HOST_EMPTY_EXIT_MS` (default 15 minutes), the host exits through its clean shutdown path (flush, socket
196
196
  removal), for stdio and `--listen` hosts alike. A connected client counts as occupancy even with no session open,
197
197
  so the host never drops a live socket under itself. Supervised hosts stay clean either way: a supervisor reads a
198
198
  child exit of 0 without a signal as an intentional idle stop and exits 0 with the same cleanup, not as a crash.
199
199
 
200
- Values are positive integers; invalid values fall through to the defaults. These bounds run inside the host process,
200
+ Values are positive integers; invalid values fall through to the defaults. These lifecycle windows run inside the host process,
201
201
  so they hold even for embedders and hand-started hosts that have no supervisor.
202
202
 
203
203
  ### D1 normative table (multi-session mode)
@@ -205,7 +205,7 @@ so they hold even for embedders and hand-started hosts that have no supervisor.
205
205
  | Command | Params | Success data | Notes |
206
206
  | --- | --- | --- | --- |
207
207
  | `get_protocol_info` | - | `{ protocolVersion: 1, serverVersion: string, capabilities: string[], mode: "classic"\|"multi" }` | Answered in BOTH modes; side-effect-free; the capability probe. Multi-session hosts include `multi_session` plus the negotiated launch capabilities. |
208
- | `open_session` | `sessionPath?`, `cwd?`, `provider?`, `modelId?`, `thinkingLevel?`, `permissionPreset?` (all optional; paths MUST be absolute) | `{ sessionId, state: RpcSessionState, attached?: true }` | `sessionPath` = today's `--session` semantics (open-if-exists else create persisting there, `session-manager.ts:926-940`); `provider`/`modelId` applied only on create (resume restores the session's model — mirrors `SenpiSessionRuntime.ts:198-200`); params form the immutable launch profile (D8). When the path is already held by a fully-open session, the open ATTACHES to it: same routing handle, `attached: true`, one more attachment counted; the runtime is torn down only when the last attachment closes. Fails with `too_many_sessions` at the occupancy cap (see [Shared host occupancy](#shared-host-occupancy-idle-eviction-session-cap-empty-host-exit)); idle sessions past the eviction window are closed by the host itself. |
208
+ | `open_session` | `sessionPath?`, `cwd?`, `provider?`, `modelId?`, `thinkingLevel?`, `permissionPreset?` (all optional; paths MUST be absolute) | `{ sessionId, state: RpcSessionState, attached?: true }` | `sessionPath` = today's `--session` semantics (open-if-exists else create persisting there, `session-manager.ts:926-940`); `provider`/`modelId` applied only on create (resume restores the session's model — mirrors `SenpiSessionRuntime.ts:198-200`); params form the immutable launch profile (D8). When the path is already held by a fully-open session, the open ATTACHES to it: same routing handle, `attached: true`, one more attachment counted; the runtime is torn down only when the last attachment closes. Idle sessions past the eviction window are closed by the host itself. |
209
209
  | `close_session` | `sessionId` | `{}` | Aborts active work, awaits agent idle + settled persistence for up to the host grace window (default 10s), then force-releases the session; its response is the LAST record tagged with that handle for the first closer — no events after (test-pinned). A concurrent close joins the same teardown and receives its own successful response. |
210
210
  | `list_sessions` | - | `{ sessions: [{ sessionId, durableSessionId, sessionPath, cwd, name, status }] }` | Includes `opening`/`closing` entries with their status. |
211
211
  | every existing command | + `sessionId` (REQUIRED in multi mode) | unchanged | Routed to that session. |
@@ -224,7 +224,6 @@ In the response `error` field, machine-matchable:
224
224
  - `missing_session_id` (session-scoped command without `sessionId` in multi mode)
225
225
  - `multi_session_disabled` (`open_session` in classic mode)
226
226
  - `invalid_path` (relative `sessionPath`/`cwd`)
227
- - `too_many_sessions` (`open_session` while `SENPI_RPC_MAX_SESSIONS` sessions are already opening/open; attaching to a live session never fails this way)
228
227
  - `open_failed: <detail>`
229
228
  - `media_not_found` (`get_media` for an unknown `toolCallId`, or a `contentIndex` that does not point at an image block)
230
229
 
@@ -12,6 +12,51 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.9.6] - 2026-09-06
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ - The Bun eval description now tells the model to shell out through `Bun.$` or `Bun.spawn` and never `Bun.spawnSync`, because a synchronous child blocks the worker and a stop or timeout then loses every variable.
24
+ - JavaScript eval cells now interrupt cooperatively: `stop` and kernel timeouts first ask the worker to settle the cell (pending bridge `tool.*` calls are rejected, `Bun.spawn` children are killed) and keep the worker VM and its globals when the cell settles within a 2 s grace; only an unsettled cell restarts the worker.
25
+
26
+ ### Fixed
27
+
28
+ - `eval({ action: "stop" })` no longer hangs when the JavaScript worker is blocked in a synchronous call such as `Bun.spawnSync`: worker termination is bounded by a 3 s deadline, a fresh worker replaces the blocked one, and the cell output names the blocked synchronous call.
29
+ - `Bun.$` commands run from a JavaScript cell no longer inherit the TUI's terminal as stdin (a stdin reader such as `cat`, an ssh or git credential prompt, or a keychain prompt blocked the cell forever); the shell wrapper isolates stdin while a cell is active without changing output, exit codes, `cwd`, `env`, or explicit stdin redirects.
30
+ - Stop results and detached-cell completion notifications report the real interrupt outcome (variables preserved, worker restarted, or outcome unknown) instead of a hardcoded per-language note.
31
+
32
+ ### Removed
33
+
34
+ ## [2026.9.5-3] - 2026-09-05
35
+
36
+ ### Breaking Changes
37
+
38
+ ### Added
39
+
40
+ ### Changed
41
+
42
+ ### Fixed
43
+
44
+ ### Removed
45
+
46
+ ## [2026.9.5-2] - 2026-09-05
47
+
48
+ ### Breaking Changes
49
+
50
+ ### Added
51
+
52
+ ### Changed
53
+
54
+ - The GPT eval dialect now routes a wait or a long run through `tool.monitor` inside the cell (the subscription line precedes the detach note, and the `## Tool Guidelines` line says so when `monitor` is reachable), so a GPT model no longer reads "long cells detach" as the way to wait on a `--watch`.
55
+
56
+ ### Fixed
57
+
58
+ ### Removed
59
+
15
60
  ## [2026.9.5] - 2026-09-05
16
61
 
17
62
  ### Breaking Changes
@@ -166,10 +166,26 @@ when the call had no summary), clearing as soon as the last detached cell settle
166
166
 
167
167
  Use `eval({ action: "peek", cell_id })` for its state and buffered output, or
168
168
  `eval({ action: "stop", cell_id })` to cancel it. Python stop interrupts the
169
- existing kernel and preserves variables. JavaScript stop kills and restarts its
170
- worker, so JavaScript VM state is lost. Detached completion messages state when
171
- kernel variables are available to the next eval cell; oversized buffered output
172
- is written under the session local root and referenced as `local://…`.
169
+ existing kernel and preserves variables. JavaScript stop is cooperative first:
170
+ the worker rejects the cell's pending bridge `tool.*` calls and kills the
171
+ `Bun.spawn` children it started, and a cell that settles within the 2 s grace
172
+ keeps the worker and every global. Only a cell that stays unsettled (a
173
+ never-resolving promise, an un-abortable `fetch`, a `Bun.$` command) costs the
174
+ worker VM. A worker blocked in a synchronous call (`Bun.spawnSync`,
175
+ `child_process.spawnSync`) cannot be stopped at all; after a 3 s termination
176
+ deadline a fresh worker replaces it, the cell output gains a stderr line naming
177
+ the blocked synchronous call, and the blocked call keeps running until it
178
+ returns. Kernel-level timeouts follow the same path. Stop results and detached
179
+ completion messages report the real outcome - variables preserved, worker
180
+ restarted, or outcome unknown - never a per-language assumption; oversized
181
+ buffered output is written under the session local root and referenced as
182
+ `local://…`.
183
+
184
+ Commands a cell runs through `Bun.$` never read the host's terminal: the worker
185
+ thread shares the TUI's stdin, so the shell wrapper hands every template an
186
+ empty pipe (`true | ( … )`) while a cell is active. Output, exit codes, `cwd`,
187
+ `env`, and explicit `< ${input}` redirects are unchanged; `Bun.spawn` and
188
+ `Bun.spawnSync` already default stdin to `/dev/null`.
173
189
 
174
190
  ## Output and artifacts
175
191
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.9.5",
3
+ "version": "2026.9.6",
4
4
  "private": true,
5
5
  "description": "Source-only senpi extension package for codemode evaluation tools",
6
6
  "type": "module",
@@ -31,14 +31,14 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@babel/parser": "8.0.4",
34
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.5",
34
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.6",
35
35
  "typebox": "1.3.18"
36
36
  },
37
37
  "peerDependencies": {
38
- "@code-yeongyu/senpi": "2026.9.5"
38
+ "@code-yeongyu/senpi": "2026.9.6"
39
39
  },
40
40
  "devDependencies": {
41
- "@code-yeongyu/senpi": "2026.9.5"
41
+ "@code-yeongyu/senpi": "2026.9.6"
42
42
  },
43
43
  "keywords": [
44
44
  "senpi",
@@ -9,3 +9,5 @@ export const RESERVED_SCHEMA_TOOL = "__schema__" as const;
9
9
  export const TIMEOUT_PAUSE_OP = "timeout-pause" as const;
10
10
  /** Canonical oh-my-pi eval-timeout resume operation. */
11
11
  export const TIMEOUT_RESUME_OP = "timeout-resume" as const;
12
+ /** Status op the JS worker emits the moment it receives `interrupt`; proves its event loop is not blocked. */
13
+ export const INTERRUPT_ACK_OP = "interrupt-ack" as const;
@@ -1,9 +1,7 @@
1
- import { dirname, join } from "node:path";
2
- import { fileURLToPath, pathToFileURL } from "node:url";
3
1
  import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
2
+ import { INTERRUPT_ACK_OP } from "../../bridge/reserved.ts";
4
3
  import type { KernelInterruptHandle } from "../../tool/types.ts";
5
- import { type CodemodeRuntimeAssetEnvironment, resolveCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
6
- import { createInlineWorker, type WorkerLike } from "./inline-worker.ts";
4
+ import { abandonedWorkerNote, awaitCooperativeSettlement, type WorkerRetirement } from "./interrupt-bounds.ts";
7
5
  import {
8
6
  assertJavaScriptKernelOpen,
9
7
  type JavaScriptKernelMode,
@@ -12,31 +10,20 @@ import {
12
10
  type ResultMessage,
13
11
  type ToolCallMessage,
14
12
  } from "./kernel-contract.ts";
15
- import { type JavaScriptKernelOptions, LocalModuleLoader, localBridgeConnection } from "./local-module-loader.ts";
13
+ import { type JavaScriptKernelOptions, LocalModuleLoader } from "./local-module-loader.ts";
16
14
  import { JavaScriptRunQueue, type PendingJavaScriptRun, stoppedResult } from "./run-queue.ts";
17
- import { bridgeError, spawnNodeWorker, WorkerStartupCancelledError, waitForReady } from "./worker-host.ts";
15
+ import { bridgeError, WorkerStartupCancelledError } from "./worker-host.ts";
16
+ import { WorkerSlot } from "./worker-slot.ts";
18
17
 
19
18
  export { JavaScriptKernelClosedError, type JavaScriptKernelMode, type JavaScriptRunInput } from "./kernel-contract.ts";
20
19
  export type { JavaScriptKernelOptions } from "./local-module-loader.ts";
21
-
22
- export interface JavaScriptWorkerEntryUrlOptions extends CodemodeRuntimeAssetEnvironment {
23
- readonly localPath?: string;
24
- }
25
-
26
- export function resolveJsWorkerEntryUrl(options: JavaScriptWorkerEntryUrlOptions = {}): URL {
27
- const localPath = options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "worker-entry.js");
28
- return pathToFileURL(resolveCodemodeRuntimeAsset(localPath, join("kernels", "js", "worker-entry.js"), options));
29
- }
20
+ export { type JavaScriptWorkerEntryUrlOptions, resolveJsWorkerEntryUrl } from "./worker-startup.ts";
30
21
 
31
22
  export class JavaScriptKernel {
32
23
  readonly #options: JavaScriptKernelOptions;
33
24
  readonly #moduleLoader: LocalModuleLoader;
34
- #worker: WorkerLike | null = null;
35
- #mode: JavaScriptKernelMode = "worker";
25
+ readonly #slot: WorkerSlot;
36
26
  #lifecycle: LifecycleState = "open";
37
- #ready: Promise<void> | null = null;
38
- #startupAbort: AbortController | null = null;
39
- #generation = 0;
40
27
  #activation: Promise<void> | null = null;
41
28
  #recovery: Promise<void> | null = null;
42
29
  #closePromise: Promise<void> | null = null;
@@ -48,10 +35,15 @@ export class JavaScriptKernel {
48
35
  constructor(options: JavaScriptKernelOptions) {
49
36
  this.#options = options;
50
37
  this.#moduleLoader = new LocalModuleLoader(options);
38
+ this.#slot = new WorkerSlot(options, {
39
+ isOpen: () => this.#lifecycle === "open",
40
+ onMessage: (message) => this.#handleMessage(message),
41
+ onCrash: (error) => this.#handleCrash(error),
42
+ });
51
43
  }
52
44
 
53
45
  get mode(): JavaScriptKernelMode {
54
- return this.#mode;
46
+ return this.#slot.mode;
55
47
  }
56
48
 
57
49
  async run(input: JavaScriptRunInput): Promise<ResultMessage> {
@@ -64,13 +56,16 @@ export class JavaScriptKernel {
64
56
  async interrupt(reason = "interrupted"): Promise<KernelInterruptHandle> {
65
57
  assertJavaScriptKernelOpen(this.#lifecycle, "interrupt");
66
58
  const active = this.#runs.active;
67
- const target = this.#runs.takeInterruptTarget();
68
- if (!target) return { stateRetained: Promise.resolve(true) };
69
- if (target === active) this.#clearTimeout();
70
- this.#runs.settle(target, stoppedResult(target.input.cellId, `JS cell interrupted: ${reason}`));
71
- await this.#restartAfterStop();
72
- // A restart always replaces the worker VM, so no user global survives.
73
- return { stateRetained: Promise.resolve(false) };
59
+ if (!active) {
60
+ const queued = this.#runs.takeInterruptTarget();
61
+ if (!queued) return { stateRetained: Promise.resolve(true) };
62
+ this.#runs.settle(queued, stoppedResult(queued.input.cellId, `JS cell interrupted: ${reason}`));
63
+ await this.#restartAfterStop();
64
+ return { stateRetained: Promise.resolve(false) };
65
+ }
66
+ this.#clearTimeout();
67
+ const stop = await this.#stopActive(active, reason, `JS cell interrupted: ${reason}`);
68
+ return { stateRetained: Promise.resolve(stop.retained), ...(stop.note === undefined ? {} : { note: stop.note }) };
74
69
  }
75
70
 
76
71
  async reset(): Promise<void> {
@@ -82,7 +77,7 @@ export class JavaScriptKernel {
82
77
  }
83
78
 
84
79
  deliverToolReply(message: Extract<HostToKernelMessage, { type: "tool-reply" }>): void {
85
- if (this.#lifecycle === "open") this.#worker?.postMessage(message);
80
+ if (this.#lifecycle === "open") this.#slot.postMessage(message);
86
81
  }
87
82
 
88
83
  async nextToolCall(): Promise<ToolCallMessage> {
@@ -93,7 +88,7 @@ export class JavaScriptKernel {
93
88
 
94
89
  async close(): Promise<void> {
95
90
  if (this.#closePromise) return await this.#closePromise;
96
- this.#worker?.postMessage({ type: "close" });
91
+ this.#slot.postMessage({ type: "close" });
97
92
  this.#lifecycle = "closing";
98
93
  this.#runs.settleAll("JS kernel closed");
99
94
  const recovery = this.#recovery;
@@ -129,92 +124,17 @@ export class JavaScriptKernel {
129
124
 
130
125
  async #ensureReady(): Promise<void> {
131
126
  assertJavaScriptKernelOpen(this.#lifecycle, "run");
132
- if (!this.#ready) {
133
- const generation = ++this.#generation;
134
- const controller = new AbortController();
135
- this.#startupAbort = controller;
136
- const ready = this.#startWorker(generation, controller.signal);
137
- this.#ready = ready;
138
- void ready.then(
139
- () => {
140
- if (this.#ready === ready) this.#startupAbort = null;
141
- },
142
- () => {
143
- if (this.#ready === ready) {
144
- this.#ready = null;
145
- this.#startupAbort = null;
146
- }
147
- },
148
- );
149
- }
150
- return await this.#ready;
151
- }
152
-
153
- async #startWorker(generation: number, signal: AbortSignal): Promise<void> {
154
- let worker = this.#spawnWorker();
155
- this.#publishWorker(worker, generation);
156
- try {
157
- await this.#initializeWorker(worker, signal);
158
- return;
159
- } catch (error) {
160
- if (!this.#isCurrent(worker, generation) || error instanceof WorkerStartupCancelledError) {
161
- await worker.terminate();
162
- throw new WorkerStartupCancelledError();
163
- }
164
- if (worker.mode === "inline") throw error;
165
- this.#worker = null;
166
- await worker.terminate();
167
- }
168
- if (this.#lifecycle !== "open" || generation !== this.#generation) throw new WorkerStartupCancelledError();
169
- worker = createInlineWorker(this.#options.cwd, this.#options.parallelPoolWidth);
170
- this.#publishWorker(worker, generation);
171
- await this.#initializeWorker(worker, signal);
172
- }
173
-
174
- #spawnWorker(): WorkerLike {
175
- try {
176
- const url = this.#options.workerEntryUrl ?? resolveJsWorkerEntryUrl();
177
- return spawnNodeWorker(url, this.#options.cwd, this.#options.parallelPoolWidth);
178
- } catch (error) {
179
- if (!(error instanceof Error)) throw error;
180
- return createInlineWorker(this.#options.cwd, this.#options.parallelPoolWidth);
181
- }
182
- }
183
-
184
- #publishWorker(worker: WorkerLike, generation: number): void {
185
- if (this.#lifecycle !== "open" || generation !== this.#generation) throw new WorkerStartupCancelledError();
186
- this.#worker = worker;
187
- this.#mode = worker.mode;
188
- worker.onMessage((message) => {
189
- if (this.#isCurrent(worker, generation)) this.#handleMessage(message);
190
- });
191
- worker.onError((error) => {
192
- if (this.#isCurrent(worker, generation)) this.#handleCrash(error);
193
- });
194
- }
195
-
196
- async #initializeWorker(worker: WorkerLike, signal: AbortSignal): Promise<void> {
197
- const ready = waitForReady(worker, signal);
198
- worker.postMessage({
199
- type: "init",
200
- sessionId: this.#options.sessionId,
201
- connection: localBridgeConnection(this.#options),
202
- });
203
- await ready;
204
- }
205
-
206
- #isCurrent(worker: WorkerLike, generation: number): boolean {
207
- return this.#lifecycle === "open" && this.#worker === worker && this.#generation === generation;
127
+ await this.#slot.ensureReady();
208
128
  }
209
129
 
210
130
  #startNext(): void {
211
- if (this.#lifecycle !== "open" || this.#runs.active || !this.#worker) return;
131
+ if (this.#lifecycle !== "open" || this.#runs.active || !this.#slot.present) return;
212
132
  const next = this.#runs.startNext(performance.now());
213
133
  if (!next) return;
214
134
  if (next.input.timeoutMs) {
215
135
  this.#timeout = setTimeout(() => void this.#timeoutActive(next), next.input.timeoutMs);
216
136
  }
217
- this.#worker.postMessage({
137
+ this.#slot.postMessage({
218
138
  type: "run",
219
139
  cellId: next.input.cellId,
220
140
  code: this.#moduleLoader.prepareCell(next.input.code),
@@ -223,21 +143,46 @@ export class JavaScriptKernel {
223
143
  }
224
144
 
225
145
  async #timeoutActive(run: PendingJavaScriptRun): Promise<void> {
226
- if (!this.#runs.releaseActive(run)) return;
146
+ if (this.#runs.active !== run || run.settled) return;
227
147
  const durationMs = run.input.timeoutMs ?? 0;
228
- this.#runs.settle(run, {
229
- type: "result",
230
- cellId: run.input.cellId,
231
- ok: false,
232
- error: { message: `JS cell timed out after ${durationMs}ms` },
148
+ await this.#stopActive(
149
+ run,
150
+ `timed out after ${durationMs}ms`,
151
+ `JS cell timed out after ${durationMs}ms`,
233
152
  durationMs,
234
- });
235
- await this.#restartAfterStop();
153
+ );
154
+ }
155
+
156
+ /**
157
+ * Asks the worker to settle the active cell cooperatively (rejecting its bridge calls and killing its
158
+ * children); only a cell that stays unsettled past the grace costs the worker VM. Reports whether the
159
+ * worker state survived and, when a blocked worker had to be abandoned, the note that explains it.
160
+ */
161
+ async #stopActive(
162
+ run: PendingJavaScriptRun,
163
+ reason: string,
164
+ message: string,
165
+ durationMs = 0,
166
+ ): Promise<{ readonly retained: boolean; readonly note?: string }> {
167
+ run.interruptResult = { type: "result", cellId: run.input.cellId, ok: false, error: { message }, durationMs };
168
+ run.interruptAck ??= Promise.withResolvers<void>();
169
+ this.#slot.postMessage({ type: "interrupt", reason });
170
+ if ((await awaitCooperativeSettlement(run)) === "settled") return { retained: run.settledByWorker };
171
+ if (!this.#runs.releaseActive(run)) return { retained: run.settledByWorker };
172
+ const retirement = await this.#terminate();
173
+ this.#runs.settle(run, run.interruptResult ?? stoppedResult(run.input.cellId, message));
174
+ void this.#recover(() => Promise.resolve());
175
+ return retirement === "abandoned" ? { retained: false, note: abandonedWorkerNote() } : { retained: false };
236
176
  }
237
177
 
238
178
  async #restartAfterStop(): Promise<void> {
179
+ await this.#recover(() => this.#terminate());
180
+ }
181
+
182
+ /** One recovery at a time: retire through `retire` (a no-op when the worker is already gone), then bring a fresh worker up. */
183
+ async #recover(retire: () => Promise<unknown>): Promise<void> {
239
184
  if (this.#recovery) return await this.#recovery;
240
- const recovery = this.#performRestartAfterStop();
185
+ const recovery = this.#performRecovery(retire);
241
186
  this.#recovery = recovery;
242
187
  try {
243
188
  await recovery;
@@ -246,9 +191,9 @@ export class JavaScriptKernel {
246
191
  }
247
192
  }
248
193
 
249
- async #performRestartAfterStop(): Promise<void> {
194
+ async #performRecovery(retire: () => Promise<unknown>): Promise<void> {
250
195
  try {
251
- await this.#terminate();
196
+ await retire();
252
197
  if (this.#lifecycle !== "open") return;
253
198
  await this.#ensureReady();
254
199
  if (this.#lifecycle === "open") this.#startNext();
@@ -259,6 +204,10 @@ export class JavaScriptKernel {
259
204
  }
260
205
 
261
206
  #handleMessage(message: KernelToHostMessage): void {
207
+ if (message.type === "status" && message.event.op === INTERRUPT_ACK_OP) {
208
+ this.#runs.active?.interruptAck?.resolve();
209
+ return;
210
+ }
262
211
  this.#options.onMessage?.(message);
263
212
  this.#runs.active?.input.onMessage?.(message);
264
213
  if (message.type === "tool-call") {
@@ -272,13 +221,14 @@ export class JavaScriptKernel {
272
221
  if (!active || active.input.cellId !== message.cellId) return;
273
222
  this.#clearTimeout();
274
223
  this.#runs.releaseActive(active);
275
- this.#runs.settle(active, message);
224
+ active.settledByWorker = true;
225
+ this.#runs.settle(active, active.interruptResult ?? message);
276
226
  this.#startNext();
277
227
  }
278
228
 
279
229
  #handleCrash(error: Error): void {
280
230
  const active = this.#runs.active;
281
- if (!active && this.#startupAbort) return;
231
+ if (!active && this.#slot.startingUp) return;
282
232
  this.#clearTimeout();
283
233
  if (active) {
284
234
  this.#runs.releaseActive(active);
@@ -298,14 +248,8 @@ export class JavaScriptKernel {
298
248
  this.#timeout = null;
299
249
  }
300
250
 
301
- async #terminate(): Promise<void> {
251
+ async #terminate(): Promise<WorkerRetirement> {
302
252
  this.#clearTimeout();
303
- this.#generation += 1;
304
- this.#startupAbort?.abort();
305
- this.#startupAbort = null;
306
- this.#ready = null;
307
- const worker = this.#worker;
308
- this.#worker = null;
309
- if (worker) await worker.terminate();
253
+ return await this.#slot.retire();
310
254
  }
311
255
  }