@code-yeongyu/senpi 2026.9.5 → 2026.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/dist/core/agent-session.d.ts +21 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +144 -56
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +6 -2
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +4 -0
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/stuck-overflow.d.ts +11 -0
- package/dist/core/compaction/stuck-overflow.d.ts.map +1 -0
- package/dist/core/compaction/stuck-overflow.js +17 -0
- package/dist/core/compaction/stuck-overflow.js.map +1 -0
- package/dist/core/extensions/builtin/claude-sdk-oauth/prompt-bridge.d.ts.map +1 -1
- package/dist/core/extensions/builtin/claude-sdk-oauth/prompt-bridge.js +2 -0
- package/dist/core/extensions/builtin/claude-sdk-oauth/prompt-bridge.js.map +1 -1
- package/dist/core/extensions/builtin/claude-sdk-oauth/session-registry-pump.d.ts.map +1 -1
- package/dist/core/extensions/builtin/claude-sdk-oauth/session-registry-pump.js +15 -2
- package/dist/core/extensions/builtin/claude-sdk-oauth/session-registry-pump.js.map +1 -1
- package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-claim.d.ts +1 -1
- package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-claim.d.ts.map +1 -1
- package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-claim.js +2 -3
- package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-claim.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts +4 -0
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +16 -19
- package/dist/core/extensions/builtin/compaction/deterministic-fallback.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +9 -0
- package/dist/core/extensions/builtin/compaction/lane-policy.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/lane-policy.js +5 -0
- package/dist/core/extensions/builtin/compaction/lane-policy.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/openai-remote-convert.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/openai-remote-convert.js +2 -0
- package/dist/core/extensions/builtin/compaction/openai-remote-convert.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/retained-message-safety.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/retained-message-safety.js +14 -1
- package/dist/core/extensions/builtin/compaction/retained-message-safety.js.map +1 -1
- package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts +1 -0
- package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/continuation-recovery.js +2 -0
- package/dist/core/extensions/builtin/goal/continuation-recovery.js.map +1 -1
- package/dist/core/extensions/builtin/goal/continuation.d.ts +3 -1
- package/dist/core/extensions/builtin/goal/continuation.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/continuation.js +4 -0
- package/dist/core/extensions/builtin/goal/continuation.js.map +1 -1
- package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts +3 -0
- package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +14 -3
- package/dist/core/extensions/builtin/goal/lifecycle-helpers.js.map +1 -1
- package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/monitor-continuation.js +2 -1
- package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
- package/dist/core/extensions/builtin/permission-system/external-dir.d.ts.map +1 -1
- package/dist/core/extensions/builtin/permission-system/external-dir.js +42 -13
- package/dist/core/extensions/builtin/permission-system/external-dir.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts +2 -2
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +8 -2
- package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js.map +1 -1
- package/dist/core/messages.d.ts +7 -0
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +2 -0
- package/dist/core/messages.js.map +1 -1
- package/dist/core/package-identity.d.ts +12 -0
- package/dist/core/package-identity.d.ts.map +1 -0
- package/dist/core/package-identity.js +57 -0
- package/dist/core/package-identity.js.map +1 -0
- package/dist/core/resource-loader.d.ts +0 -2
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +8 -60
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/retry-fallback/controller.d.ts.map +1 -1
- package/dist/core/retry-fallback/controller.js +5 -4
- package/dist/core/retry-fallback/controller.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +5 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +12 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +29 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +6 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +5 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/multi-session-host.d.ts +3 -7
- package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
- package/dist/modes/rpc/multi-session-host.js +3 -13
- package/dist/modes/rpc/multi-session-host.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +1 -2
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js +0 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/modes/rpc/session-registry.d.ts +1 -4
- package/dist/modes/rpc/session-registry.d.ts.map +1 -1
- package/dist/modes/rpc/session-registry.js +0 -15
- package/dist/modes/rpc/session-registry.js.map +1 -1
- package/docs/compaction-guide.md +2 -2
- package/docs/compaction.md +2 -2
- package/docs/rpc.md +6 -7
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +57 -0
- package/node_modules/@code-yeongyu/senpi-codemode/README.md +20 -4
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/reserved.ts +2 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/jl/kernel.ts +2 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +73 -129
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/inline-worker.ts +2 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/interrupt-bounds.ts +66 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/run-queue.ts +18 -3
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-core.js +30 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-runtime.js +16 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-shell-capture.d.ts +9 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-shell-capture.js +32 -6
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-slot.ts +106 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-startup.ts +69 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/transport.ts +2 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/rb/kernel.ts +2 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/runtime-asset.ts +40 -14
- package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +10 -5
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-execution.ts +9 -11
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-contract.ts +45 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-manager.ts +43 -71
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-notification.ts +4 -5
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-snapshot.ts +2 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-status.ts +30 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-eval-result.ts +1 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-notification-queue.ts +2 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/interrupt-note.ts +29 -15
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +2 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +3 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js +17 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js +1 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +1 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/reducer.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/reducer.js +6 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/reducer.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/context.d.ts +2 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/context.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/context.js +5 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/context.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl/codec.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl/codec.js +1 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl/codec.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/types.d.ts +7 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/types.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/types.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts +2 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/types.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/mistral-conversations.js +2 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/mistral-conversations.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js +15 -0
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +4 -4
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/azure-openai-responses.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cerebras.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai-cn.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai-codex.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opengateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/faux.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/faux.js +2 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/faux.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +8 -1
- package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.js +2 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
|
@@ -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 {
|
|
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
|
|
13
|
+
import { type JavaScriptKernelOptions, LocalModuleLoader } from "./local-module-loader.ts";
|
|
16
14
|
import { JavaScriptRunQueue, type PendingJavaScriptRun, stoppedResult } from "./run-queue.ts";
|
|
17
|
-
import { bridgeError,
|
|
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
|
-
#
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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 (
|
|
146
|
+
if (this.#runs.active !== run || run.settled) return;
|
|
227
147
|
const durationMs = run.input.timeoutMs ?? 0;
|
|
228
|
-
this.#
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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.#
|
|
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 #
|
|
194
|
+
async #performRecovery(retire: () => Promise<unknown>): Promise<void> {
|
|
250
195
|
try {
|
|
251
|
-
await
|
|
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
|
-
|
|
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.#
|
|
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<
|
|
251
|
+
async #terminate(): Promise<WorkerRetirement> {
|
|
302
252
|
this.#clearTimeout();
|
|
303
|
-
this.#
|
|
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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
3
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
4
|
-
import { type CodemodeRuntimeAssetEnvironment,
|
|
4
|
+
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
5
5
|
import type { JavaScriptKernelMode } from "./kernel-contract.ts";
|
|
6
6
|
import { spawnNodeWorker } from "./worker-host.ts";
|
|
7
7
|
|
|
@@ -12,7 +12,7 @@ export interface JavaScriptInlineWorkerEntryUrlOptions extends CodemodeRuntimeAs
|
|
|
12
12
|
export function resolveInlineWorkerEntryUrl(options: JavaScriptInlineWorkerEntryUrlOptions = {}): URL {
|
|
13
13
|
const localPath = options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "inline-worker-entry.js");
|
|
14
14
|
return pathToFileURL(
|
|
15
|
-
|
|
15
|
+
requireCodemodeRuntimeAsset(localPath, join("kernels", "js", "inline-worker-entry.js"), options),
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { WorkerLike } from "./inline-worker.ts";
|
|
2
|
+
import type { PendingJavaScriptRun } from "./run-queue.ts";
|
|
3
|
+
|
|
4
|
+
/** How long the worker gets to acknowledge `interrupt`; silence means its event loop is blocked in synchronous code. */
|
|
5
|
+
export const INTERRUPT_ACK_MS = 500;
|
|
6
|
+
/** How long an acknowledged cell gets to settle before the VM is replaced. */
|
|
7
|
+
export const JS_INTERRUPT_GRACE_MS = 2_000;
|
|
8
|
+
/** How long `worker.terminate()` may take before the worker is abandoned as blocked in a synchronous call. */
|
|
9
|
+
export const WORKER_TERMINATE_DEADLINE_MS = 3_000;
|
|
10
|
+
|
|
11
|
+
export type CooperativeSettlement = "settled" | "unresponsive";
|
|
12
|
+
export type WorkerRetirement = "terminated" | "abandoned";
|
|
13
|
+
|
|
14
|
+
export interface CooperativeSettlementBounds {
|
|
15
|
+
readonly ackMs: number;
|
|
16
|
+
readonly graceMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_SETTLEMENT_BOUNDS: CooperativeSettlementBounds = {
|
|
20
|
+
ackMs: INTERRUPT_ACK_MS,
|
|
21
|
+
graceMs: JS_INTERRUPT_GRACE_MS,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function awaitCooperativeSettlement(
|
|
25
|
+
run: PendingJavaScriptRun,
|
|
26
|
+
bounds = DEFAULT_SETTLEMENT_BOUNDS,
|
|
27
|
+
): Promise<CooperativeSettlement> {
|
|
28
|
+
if (run.settled) return "settled";
|
|
29
|
+
const settled = run.settlement.then((): "settled" => "settled");
|
|
30
|
+
const acked = run.interruptAck?.promise.then((): "acked" => "acked") ?? Promise.resolve<"acked">("acked");
|
|
31
|
+
const first = await raceDeadline(Promise.race([settled, acked]), bounds.ackMs, "unresponsive");
|
|
32
|
+
if (first !== "acked") return first;
|
|
33
|
+
return await raceDeadline(settled, bounds.graceMs, "unresponsive");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function retireWorker(
|
|
37
|
+
worker: WorkerLike,
|
|
38
|
+
deadlineMs = WORKER_TERMINATE_DEADLINE_MS,
|
|
39
|
+
): Promise<WorkerRetirement> {
|
|
40
|
+
const termination = worker.terminate().then((): WorkerRetirement => "terminated");
|
|
41
|
+
const outcome = await raceDeadline(termination, deadlineMs, "abandoned");
|
|
42
|
+
if (outcome === "abandoned") void termination.then(undefined, ignoreLateTerminationFailure);
|
|
43
|
+
return outcome;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function abandonedWorkerNote(deadlineMs = WORKER_TERMINATE_DEADLINE_MS): string {
|
|
47
|
+
return `JavaScript worker did not stop within ${deadlineMs}ms: a synchronous call (for example Bun.spawnSync or child_process.spawnSync) is blocking it. A fresh worker replaced it; the blocked call keeps running until it returns.\n`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function raceDeadline<T extends string, Fallback extends string>(
|
|
51
|
+
operation: Promise<T>,
|
|
52
|
+
deadlineMs: number,
|
|
53
|
+
fallback: Fallback,
|
|
54
|
+
): Promise<T | Fallback> {
|
|
55
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
56
|
+
const deadline = new Promise<Fallback>((resolve) => {
|
|
57
|
+
timer = setTimeout(() => resolve(fallback), deadlineMs);
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
return await Promise.race([operation, deadline]);
|
|
61
|
+
} finally {
|
|
62
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ignoreLateTerminationFailure(): void {}
|
|
@@ -7,8 +7,13 @@ export interface PendingJavaScriptRun {
|
|
|
7
7
|
readonly input: JavaScriptRunInput;
|
|
8
8
|
readonly resolve: (message: ResultMessage) => void;
|
|
9
9
|
readonly reject: (error: Error) => void;
|
|
10
|
+
readonly settlement: Promise<ResultMessage>;
|
|
10
11
|
startedAtMs: number | null;
|
|
11
12
|
settled: boolean;
|
|
13
|
+
/** Host-composed result that wins over whatever the worker reports once an interrupt is in flight. */
|
|
14
|
+
interruptResult: ResultMessage | null;
|
|
15
|
+
interruptAck: PromiseWithResolvers<void> | null;
|
|
16
|
+
settledByWorker: boolean;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export class JavaScriptRunQueue {
|
|
@@ -24,9 +29,19 @@ export class JavaScriptRunQueue {
|
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
enqueue(input: JavaScriptRunInput): Promise<ResultMessage> {
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
const { promise, resolve, reject } = Promise.withResolvers<ResultMessage>();
|
|
33
|
+
this.#queue.push({
|
|
34
|
+
input,
|
|
35
|
+
resolve,
|
|
36
|
+
reject,
|
|
37
|
+
settlement: promise,
|
|
38
|
+
startedAtMs: null,
|
|
39
|
+
settled: false,
|
|
40
|
+
interruptResult: null,
|
|
41
|
+
interruptAck: null,
|
|
42
|
+
settledByWorker: false,
|
|
29
43
|
});
|
|
44
|
+
return promise;
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
startNext(startedAtMs: number): PendingJavaScriptRun | null {
|
|
@@ -64,7 +79,7 @@ export class JavaScriptRunQueue {
|
|
|
64
79
|
settleAll(message: string): void {
|
|
65
80
|
const active = this.#active;
|
|
66
81
|
this.#active = null;
|
|
67
|
-
if (active) this.settle(active, stoppedResult(active.input.cellId, message));
|
|
82
|
+
if (active) this.settle(active, active.interruptResult ?? stoppedResult(active.input.cellId, message));
|
|
68
83
|
for (const queued of this.#queue.splice(0)) this.settle(queued, stoppedResult(queued.input.cellId, message));
|
|
69
84
|
}
|
|
70
85
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { JsWorkerRuntime } from "./worker-runtime.js";
|
|
2
2
|
|
|
3
|
+
// Mirrors INTERRUPT_ACK_OP in src/bridge/reserved.ts (this worker file cannot import TypeScript).
|
|
4
|
+
const INTERRUPT_ACK_OP = "interrupt-ack";
|
|
5
|
+
|
|
3
6
|
export function createWorkerCore(transport, options) {
|
|
4
7
|
let runtime = null;
|
|
8
|
+
let activeCell = null;
|
|
5
9
|
const pendingTools = new Map();
|
|
6
10
|
|
|
7
11
|
function emit(message) {
|
|
@@ -14,6 +18,7 @@ export function createWorkerCore(transport, options) {
|
|
|
14
18
|
return;
|
|
15
19
|
}
|
|
16
20
|
const startedAtMs = performance.now();
|
|
21
|
+
activeCell = { cellId: message.cellId, interruption: null };
|
|
17
22
|
try {
|
|
18
23
|
const value = await runtime.run(message.code, message.cellId, {
|
|
19
24
|
emit,
|
|
@@ -22,16 +27,31 @@ export function createWorkerCore(transport, options) {
|
|
|
22
27
|
emit({ type: "result", cellId: message.cellId, ok: true, valueRepr: valueRepr(value), durationMs: durationMs(startedAtMs) });
|
|
23
28
|
} catch (error) {
|
|
24
29
|
emit({ type: "result", cellId: message.cellId, ok: false, error: bridgeError(error), durationMs: durationMs(startedAtMs) });
|
|
30
|
+
} finally {
|
|
31
|
+
activeCell = null;
|
|
25
32
|
}
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
async function callTool(toolName, args) {
|
|
36
|
+
if (activeCell?.interruption) throw activeCell.interruption;
|
|
29
37
|
const callId = `js-${crypto.randomUUID()}`;
|
|
30
38
|
const promise = new Promise((resolve, reject) => pendingTools.set(callId, { resolve, reject }));
|
|
31
39
|
emit({ type: "tool-call", callId, toolName, args });
|
|
32
40
|
return await promise;
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
function interruptCell(reason) {
|
|
44
|
+
if (!activeCell || !runtime) return;
|
|
45
|
+
emit({ type: "status", event: { op: INTERRUPT_ACK_OP, cellId: activeCell.cellId } });
|
|
46
|
+
const interruption = cellInterruptedError(reason);
|
|
47
|
+
activeCell.interruption = interruption;
|
|
48
|
+
for (const [callId, pending] of pendingTools) {
|
|
49
|
+
pendingTools.delete(callId);
|
|
50
|
+
pending.reject(interruption);
|
|
51
|
+
}
|
|
52
|
+
runtime.interrupt();
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
function onMessage(message) {
|
|
36
56
|
if (message.type === "init") {
|
|
37
57
|
runtime = new JsWorkerRuntime({
|
|
@@ -55,6 +75,10 @@ export function createWorkerCore(transport, options) {
|
|
|
55
75
|
else pending.reject(errorFromBridge(message.error));
|
|
56
76
|
return;
|
|
57
77
|
}
|
|
78
|
+
if (message.type === "interrupt") {
|
|
79
|
+
interruptCell(message.reason ?? "interrupted");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
58
82
|
if (message.type === "close") {
|
|
59
83
|
emit({ type: "closed" });
|
|
60
84
|
transport.close();
|
|
@@ -79,6 +103,12 @@ function valueRepr(value) {
|
|
|
79
103
|
return JSON.stringify(value);
|
|
80
104
|
}
|
|
81
105
|
|
|
106
|
+
function cellInterruptedError(reason) {
|
|
107
|
+
const error = new Error(`JS cell interrupted: ${reason}`);
|
|
108
|
+
error.name = "CellInterruptedError";
|
|
109
|
+
return error;
|
|
110
|
+
}
|
|
111
|
+
|
|
82
112
|
function bridgeError(error) {
|
|
83
113
|
if (error instanceof Error) {
|
|
84
114
|
return { name: error.name, message: error.message, stack: error.stack };
|
|
@@ -16,6 +16,7 @@ export class JsWorkerRuntime {
|
|
|
16
16
|
#env = new Map();
|
|
17
17
|
#hooks = null;
|
|
18
18
|
#pendingDisplays = [];
|
|
19
|
+
#children = new Set();
|
|
19
20
|
|
|
20
21
|
constructor(options) {
|
|
21
22
|
this.#cwd = options.cwd;
|
|
@@ -41,10 +42,24 @@ export class JsWorkerRuntime {
|
|
|
41
42
|
return value;
|
|
42
43
|
} finally {
|
|
43
44
|
this.#pendingDisplays = [];
|
|
45
|
+
this.#children.clear();
|
|
44
46
|
this.#hooks = null;
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
interrupt() {
|
|
51
|
+
for (const child of this.#children) {
|
|
52
|
+
if (child.exitCode === null && child.signalCode === null) child.kill();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#trackChild(child) {
|
|
57
|
+
if (child === null || typeof child !== "object" || typeof child.kill !== "function") return;
|
|
58
|
+
this.#children.add(child);
|
|
59
|
+
const forget = () => this.#children.delete(child);
|
|
60
|
+
if (child.exited instanceof Promise) child.exited.then(forget, forget);
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
async #drainPendingDisplays() {
|
|
49
64
|
while (this.#pendingDisplays.length > 0) {
|
|
50
65
|
const pending = this.#pendingDisplays;
|
|
@@ -99,6 +114,7 @@ export class JsWorkerRuntime {
|
|
|
99
114
|
const restoreShellCapture = installShellCapture({
|
|
100
115
|
isActive: () => this.#hooks !== null,
|
|
101
116
|
emitText: (stream, data) => this.#emitText(stream, data),
|
|
117
|
+
onChild: (child) => this.#trackChild(child),
|
|
102
118
|
});
|
|
103
119
|
globalThis.__senpi_restore_console__ = () => {
|
|
104
120
|
console.log = originalLog;
|
|
@@ -2,9 +2,18 @@ export type ShellCaptureStream = "stdout" | "stderr";
|
|
|
2
2
|
|
|
3
3
|
export type ShellCaptureRestore = () => void;
|
|
4
4
|
|
|
5
|
+
export interface ShellCaptureChild {
|
|
6
|
+
readonly exitCode: number | null;
|
|
7
|
+
readonly signalCode: string | null;
|
|
8
|
+
readonly exited: Promise<number>;
|
|
9
|
+
kill(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export interface ShellCaptureOptions {
|
|
6
13
|
readonly isActive: () => boolean;
|
|
7
14
|
readonly emitText: (stream: ShellCaptureStream, data: string) => void;
|
|
15
|
+
/** Receives every `Bun.spawn` child created while a cell is active so the runtime can kill it on interrupt. */
|
|
16
|
+
readonly onChild?: (child: ShellCaptureChild) => void;
|
|
8
17
|
}
|
|
9
18
|
|
|
10
19
|
export function installShellCapture(options: ShellCaptureOptions): ShellCaptureRestore;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
const SHELL_CONFIG_METHODS = ["env", "cwd", "nothrow", "throws"];
|
|
2
2
|
const SHELL_READ_METHODS = ["text", "json", "lines", "arrayBuffer", "bytes", "blob"];
|
|
3
|
+
// `true | ( … )` hands every command in the template an empty pipe as stdin. The worker thread shares
|
|
4
|
+
// the host process's fd 0 (the TUI's terminal), which Bun.$ would otherwise inherit, so a stdin
|
|
5
|
+
// reader would wait on the user's keyboard forever. The newline before `)` keeps a trailing comment
|
|
6
|
+
// from swallowing the closing paren; the Bun shell has no other stdin control (no `$.stdin`, no
|
|
7
|
+
// redirect on a subshell).
|
|
8
|
+
const STDIN_ISOLATION_HEAD = "true | (\n";
|
|
9
|
+
const STDIN_ISOLATION_TAIL = "\n)";
|
|
3
10
|
|
|
4
11
|
export function installShellCapture(options) {
|
|
5
12
|
const bun = globalThis.Bun;
|
|
@@ -20,8 +27,9 @@ function isBunRuntime(bun) {
|
|
|
20
27
|
|
|
21
28
|
function capturedShell(originalShell, options) {
|
|
22
29
|
const shell = (strings, ...expressions) => {
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
if (!options.isActive()) return originalShell(strings, ...expressions);
|
|
31
|
+
const promise = originalShell(isolateStdin(strings), ...expressions);
|
|
32
|
+
return captureShellPromise(promise, options.emitText);
|
|
25
33
|
};
|
|
26
34
|
for (const key of Object.keys(originalShell)) shell[key] = originalShell[key];
|
|
27
35
|
for (const method of SHELL_CONFIG_METHODS) {
|
|
@@ -33,6 +41,18 @@ function capturedShell(originalShell, options) {
|
|
|
33
41
|
return shell;
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
function isolateStdin(strings) {
|
|
45
|
+
if (!Array.isArray(strings) || !Array.isArray(strings.raw)) return strings;
|
|
46
|
+
const cooked = [...strings];
|
|
47
|
+
const raw = [...strings.raw];
|
|
48
|
+
const last = cooked.length - 1;
|
|
49
|
+
cooked[0] = `${STDIN_ISOLATION_HEAD}${cooked[0]}`;
|
|
50
|
+
raw[0] = `${STDIN_ISOLATION_HEAD}${raw[0]}`;
|
|
51
|
+
cooked[last] = `${cooked[last]}${STDIN_ISOLATION_TAIL}`;
|
|
52
|
+
raw[last] = `${raw[last]}${STDIN_ISOLATION_TAIL}`;
|
|
53
|
+
return Object.freeze(Object.assign(cooked, { raw: Object.freeze(raw) }));
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
function captureShellPromise(promise, emitText) {
|
|
37
57
|
const prototype = Object.getPrototypeOf(promise);
|
|
38
58
|
let echo = true;
|
|
@@ -87,13 +107,19 @@ function capturedSpawn(originalSpawn, options) {
|
|
|
87
107
|
return (...args) => {
|
|
88
108
|
if (!options.isActive()) return originalSpawn(...args);
|
|
89
109
|
const [first, second] = args;
|
|
110
|
+
let child;
|
|
90
111
|
if (Array.isArray(first)) {
|
|
91
112
|
const spawnOptions = second === undefined ? {} : second;
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
child = needsStderrCapture(spawnOptions)
|
|
114
|
+
? drainStderr(originalSpawn(first, { ...spawnOptions, stderr: "pipe" }), options.emitText)
|
|
115
|
+
: originalSpawn(...args);
|
|
116
|
+
} else {
|
|
117
|
+
child = needsStderrCapture(first)
|
|
118
|
+
? drainStderr(originalSpawn({ ...first, stderr: "pipe" }), options.emitText)
|
|
119
|
+
: originalSpawn(...args);
|
|
94
120
|
}
|
|
95
|
-
|
|
96
|
-
return
|
|
121
|
+
options.onChild?.(child);
|
|
122
|
+
return child;
|
|
97
123
|
};
|
|
98
124
|
}
|
|
99
125
|
|