@springbrand/agent-runtime 0.3.0-alpha.24 → 0.3.0-alpha.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.3.0-alpha.24",
3
+ "version": "0.3.0-alpha.26",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -36,7 +36,7 @@
36
36
  "zod": "^4.4.3",
37
37
  "@earendil-works/pi-durable": "1.1.0",
38
38
  "@earendil-works/chord": "1.1.0",
39
- "@springbrand/model-client": "0.1.0-alpha.1"
39
+ "@springbrand/model-client": "0.1.0-alpha.2"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "agents": ">=0.28.1 <0.29.0",
@@ -264,7 +264,16 @@ export interface WorkspaceStreamWriteOptions {
264
264
  */
265
265
  export interface RuntimeCodeExecutionPort {
266
266
  readonly description: string;
267
- execute(input: { code: string }): Promise<AgentToolResult<unknown>>;
267
+ /**
268
+ * Honour a provided signal before dispatch and throughout execution.
269
+ * On abort, stop suspended sandbox continuations and further Host dispatches,
270
+ * await execution finalization, and reject rather than accepting a late result.
271
+ * Already-started Host effects are not rolled back; CPU preemption is not guaranteed.
272
+ */
273
+ execute(
274
+ input: { code: string },
275
+ options?: { signal?: AbortSignal },
276
+ ): Promise<AgentToolResult<unknown>>;
268
277
  }
269
278
 
270
279
  /**
@@ -23,7 +23,8 @@ export type ThinkingEffort =
23
23
  | "low"
24
24
  | "medium"
25
25
  | "high"
26
- | "xhigh";
26
+ | "xhigh"
27
+ | "max";
27
28
 
28
29
  /**
29
30
  * 保存一次装配选定的内存开关与上下文预算。
@@ -34,6 +34,30 @@ codemode/adapter.ts Cloudflare 执行器组装、描述与结果适
34
34
 
35
35
  `pi-to-ai.ts` 也被 Skill 脚本执行使用,因此放在共用 `adapters/` 中。工具声明仍以 Pi 为唯一事实源。
36
36
 
37
+ ### Cancellation And Settlement
38
+
39
+ `RuntimeCodeExecutionPort.execute(input, { signal })` carries cancellation
40
+ through the adapter to the invocation's native sandbox. Parent cancellation
41
+ and the Tool deadline share this path. The Port awaits sandbox finalization
42
+ and connector cleanup before rejecting; it does not abandon a running Promise.
43
+
44
+ `codemode/executor.ts` composes the public Cloudflare executor with a retained,
45
+ per-invocation RPC cancellation handle. It aborts the sandbox invocation, never
46
+ the parent Host context or a sibling invocation. This does not guarantee CPU
47
+ preemption, Worker destruction, quota release or rollback of Host effects.
48
+
49
+ Already-started Host work can finish after sandbox cancellation. Its returned
50
+ facts must still reach the completion callback and the official connector's
51
+ durable result history. That does not permit model continuation or accept a
52
+ cancelled Tool's termination control. Producer-owned usage events remain
53
+ authoritative; an aborted outer Tool must not be interpreted as zero usage.
54
+
55
+ Browser invocations each own an `InvocationBrowserConnector`. At pass end it
56
+ rejects new dispatches and drains its already-started browser calls before the
57
+ public SDK closes the socket and one-shot session. An allocation can complete
58
+ after sandbox abort; the Port waits for that late allocation and its cleanup
59
+ rather than reporting terminal cleanup early. Other Host tools are not drained.
60
+
37
61
  ## 修改位置
38
62
 
39
63
  - 修改某个工具:进入对应功能目录,测试与实现放在一起。
@@ -4,7 +4,10 @@ import { tool as aiTool, jsonSchema, type ToolSet } from "ai";
4
4
  import type { PiToolCandidate } from "../runtime/contracts";
5
5
 
6
6
  interface NestedToolOptions {
7
+ /** Bound by the execution when the upstream connector omits AI SDK call options. */
8
+ readonly signal?: AbortSignal;
7
9
  readonly fallbackToolCallId?: (name: string) => string;
10
+ /** Settled Host facts, even after cancellation; not permission for model control. */
8
11
  readonly onResult?: (result: AgentToolResult<unknown>) => void;
9
12
  }
10
13
 
@@ -19,6 +22,11 @@ export function piCandidatesToAiTools(
19
22
  description: candidate.tool.description,
20
23
  inputSchema: jsonSchema(candidate.tool.parameters as never),
21
24
  execute: async (input, call) => {
25
+ const callSignal = call?.abortSignal;
26
+ const signal = options.signal && callSignal && options.signal !== callSignal
27
+ ? AbortSignal.any([options.signal, callSignal])
28
+ : options.signal ?? callSignal ?? new AbortController().signal;
29
+ signal.throwIfAborted();
22
30
  const toolCallId = call?.toolCallId ??
23
31
  options.fallbackToolCallId?.(candidate.tool.name) ??
24
32
  candidate.tool.name;
@@ -29,11 +37,15 @@ export function piCandidatesToAiTools(
29
37
  name: candidate.tool.name,
30
38
  arguments: prepared as JsonObject,
31
39
  });
40
+ signal.throwIfAborted();
32
41
  const result = await candidate.tool.execute(
33
42
  toolCallId,
34
43
  args,
35
- call?.abortSignal ?? new AbortController().signal,
44
+ signal,
36
45
  );
46
+ // A resolved Host operation is a fact even if cancellation happened
47
+ // while it ran. Let settlement and the connector's durable recordResult
48
+ // finish; the outer Code Mode boundary rejects model continuation.
37
49
  options.onResult?.(result);
38
50
  if (result.details !== undefined) return result.details;
39
51
  const [only] = result.content;
@@ -1,4 +1,4 @@
1
- import { truncateResponse, truncateResult, type ProxyToolOutput } from "@cloudflare/codemode";
1
+ import { createCodemodeRuntime, truncateResponse, truncateResult, type ProxyToolOutput } from "@cloudflare/codemode";
2
2
  import { generateTypes } from "@cloudflare/codemode/ai";
3
3
  import {
4
4
  STATE_TYPES,
@@ -6,10 +6,10 @@ import {
6
6
  type StateBackend,
7
7
  type WorkspaceFsLike,
8
8
  } from "@cloudflare/shell";
9
- import { createBrowserTools } from "@cloudflare/think/tools/browser";
10
9
  import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
11
10
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
12
11
  import type { Usage } from "@earendil-works/pi-ai";
12
+ import { DurableBrowserSessionStore } from "agents/browser";
13
13
  import type { ToolSet } from "ai";
14
14
  import type {
15
15
  RuntimeBrowserPort,
@@ -19,6 +19,8 @@ import type {
19
19
  } from "../../config/ports";
20
20
  import { serializeOutput } from "../../lib/artifacts";
21
21
  import { piCandidatesToAiTools } from "../adapters/pi-to-ai";
22
+ import { InvocationBrowserConnector } from "./browser-connector";
23
+ import { createCodeExecutionExecutor } from "./executor";
22
24
  import {
23
25
  WORKSPACE_PAGING_INSTRUCTIONS,
24
26
  WORKSPACE_STATE_TYPES,
@@ -226,11 +228,23 @@ function toCodeExecutionPort(
226
228
  }
227
229
  return {
228
230
  description,
229
- execute: async (input) => project(await execute(input, {
230
- toolCallId: name,
231
- messages: [],
232
- context: undefined,
233
- })),
231
+ execute: async (input, options) => {
232
+ const signal = options?.signal;
233
+ signal?.throwIfAborted();
234
+ try {
235
+ const output = await execute(input, {
236
+ toolCallId: name,
237
+ messages: [],
238
+ context: undefined,
239
+ abortSignal: signal,
240
+ });
241
+ signal?.throwIfAborted();
242
+ return project(output);
243
+ } catch (error) {
244
+ signal?.throwIfAborted();
245
+ throw error;
246
+ }
247
+ },
234
248
  };
235
249
  }
236
250
 
@@ -292,30 +306,38 @@ export function createBrowserExecutionFactory(options: {
292
306
  }): RuntimeBrowserPort {
293
307
  return {
294
308
  create() {
295
- const tools = createBrowserTools({
296
- ctx: options.ctx,
297
- loader: options.loader,
298
- browser: options.browser,
299
- // 沙箱超时沿用 Code Mode 的同一个常量,先于 60s 外层截止结束。
300
- timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
301
- // 会话用上游默认的 one-shot:每次执行一个全新浏览器,两次自检互不污染。
302
- // Quick Action 显式关闭:2026-08-13 复测本地绑定仍不实现 quickAction(),
303
- // 让上游默认值把那四个工具带回来只会注册出本地必然失败的 Tool(knowledge F22)。
304
- quickActions: false,
305
- name: "browser",
306
- });
307
- const tool = tools.browser_execute;
308
- if (!tool) {
309
- throw new Error("Upstream createBrowserTools did not return browser_execute");
310
- }
311
- const port = toCodeExecutionPort(tool, "browser_execute");
312
- const description = `${port.description}\n\n${browserToolDescription()}`;
313
- if (!options.previewBaseUrl) return { ...port, description };
314
- // 前置而不是追加:上游的 codemode 描述很长,缀在末尾的一句会被读丢 ——
315
- // 2026-08-13 手动冒烟里模型就没看见它,转而去猜 `file://` 和 `https://workspace.local/`。
309
+ // Compose public seams with per-invocation ownership. No Quick Actions.
310
+ const createPort = (signal?: AbortSignal) => {
311
+ const connector = new InvocationBrowserConnector(options.ctx, {
312
+ browser: options.browser,
313
+ store: new DurableBrowserSessionStore(options.ctx.storage),
314
+ timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
315
+ // Keep the public connector's default one-shot lifecycle.
316
+ });
317
+ const runtime = createCodemodeRuntime({
318
+ ctx: options.ctx,
319
+ executor: createCodeExecutionExecutor({
320
+ loader: options.loader,
321
+ timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
322
+ }, signal),
323
+ connectors: [connector],
324
+ name: "browser",
325
+ // Keep raw screenshots intact for browserResult's Pi image projection.
326
+ });
327
+ return toCodeExecutionPort(runtime.tool({
328
+ connectorHints: {
329
+ cdp: "Browser CDP. Return screenshots as { type: 'browser_screenshot', mediaType: 'image/png', data: screenshot.data }; the UI keeps the image while the model receives a compact summary.",
330
+ },
331
+ }), "browser_execute");
332
+ };
333
+ const description = `${createPort().description}\n\n${browserToolDescription()}`;
316
334
  return {
317
- ...port,
318
- description: `${previewNote(options.previewBaseUrl)}\n\n${description}`,
335
+ // Keep the Session's exact preview URL ahead of the long generated guidance.
336
+ description: options.previewBaseUrl
337
+ ? `${previewNote(options.previewBaseUrl)}\n\n${description}`
338
+ : description,
339
+ execute: (input, executionOptions) =>
340
+ createPort(executionOptions?.signal).execute(input, executionOptions),
319
341
  };
320
342
  },
321
343
  };
@@ -343,9 +365,11 @@ export function createWorkspaceCodeExecutionFactory(options: {
343
365
  : undefined;
344
366
  const createPort = (
345
367
  innerResults: AgentToolResult<unknown>[],
368
+ signal?: AbortSignal,
346
369
  ) => {
347
370
  let nestedCallOrdinal = 0;
348
371
  const tools = piCandidatesToAiTools(candidates, {
372
+ signal,
349
373
  fallbackToolCallId: (name) =>
350
374
  `codemode:${++nestedCallOrdinal}:${name}`,
351
375
  onResult: (value) => innerResults.push(value),
@@ -354,6 +378,11 @@ export function createWorkspaceCodeExecutionFactory(options: {
354
378
  ctx: options.ctx,
355
379
  loader: options.loader,
356
380
  globalOutbound: options.outbound,
381
+ executor: createCodeExecutionExecutor({
382
+ loader: options.loader,
383
+ globalOutbound: options.outbound,
384
+ timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
385
+ }, signal),
357
386
  tools,
358
387
  timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
359
388
  connectors: workspaceState ? [workspaceState] : [],
@@ -392,7 +421,8 @@ export function createWorkspaceCodeExecutionFactory(options: {
392
421
  ...(state && options.workspace.readFilePage ? [WORKSPACE_PAGING_INSTRUCTIONS] : []),
393
422
  SPRINGBRAND_CODE_MODE_POLICY,
394
423
  ].join("\n\n"),
395
- execute: (input) => createPort([]).execute(input),
424
+ execute: (input, executionOptions) =>
425
+ createPort([], executionOptions?.signal).execute(input, executionOptions),
396
426
  };
397
427
  },
398
428
  };
@@ -0,0 +1,28 @@
1
+ import { BrowserConnector } from "agents/browser";
2
+
3
+ /** Drains one invocation's browser calls before the upstream pass lifecycle ends. */
4
+ export class InvocationBrowserConnector extends BrowserConnector {
5
+ readonly #pending = new Set<Promise<unknown>>();
6
+ #ending = false;
7
+
8
+ override async executeTool(
9
+ ...args: Parameters<BrowserConnector["executeTool"]>
10
+ ): Promise<unknown> {
11
+ if (this.#ending) throw new Error("Browser execution has ended");
12
+ const pending = super.executeTool(...args);
13
+ this.#pending.add(pending);
14
+ try {
15
+ return await pending;
16
+ } finally {
17
+ this.#pending.delete(pending);
18
+ }
19
+ }
20
+
21
+ override async onPassEnd(
22
+ ...args: Parameters<BrowserConnector["onPassEnd"]>
23
+ ): Promise<void> {
24
+ this.#ending = true;
25
+ await Promise.allSettled(this.#pending);
26
+ await super.onPassEnd(...args);
27
+ }
28
+ }
@@ -0,0 +1,173 @@
1
+ /// <reference lib="esnext.disposable" />
2
+ import {
3
+ DynamicWorkerExecutor,
4
+ type ConnectorBinding,
5
+ type DynamicWorkerExecutorOptions,
6
+ type ExecuteResult,
7
+ type Executor,
8
+ type ResolvedProvider,
9
+ } from "@cloudflare/codemode";
10
+ import { RpcTarget } from "cloudflare:workers";
11
+
12
+ const CONTROL = "__springbrand_execution_control";
13
+ const SETUP = "__springbrand_execution_setup";
14
+ const CANCELLED = "Code execution cancelled";
15
+
16
+ interface AbortHandle {
17
+ abort(): Promise<void>;
18
+ dup(): AbortHandle;
19
+ [Symbol.dispose](): void;
20
+ }
21
+
22
+ function isAbortHandle(value: unknown): value is AbortHandle {
23
+ return (typeof value === "object" && value !== null || typeof value === "function") &&
24
+ typeof Reflect.get(value, "abort") === "function" &&
25
+ typeof Reflect.get(value, "dup") === "function" &&
26
+ typeof Reflect.get(value, Symbol.dispose) === "function";
27
+ }
28
+
29
+ class ExecutionControl extends RpcTarget {
30
+ #handle?: AbortHandle;
31
+ #registered = false;
32
+ #requested = false;
33
+ #aborting?: Promise<void>;
34
+ failure?: unknown;
35
+
36
+ constructor(readonly abortMessage: string) {
37
+ super();
38
+ }
39
+
40
+ async callTool(method: string, value: unknown): Promise<void> {
41
+ if (method !== "register" || this.#registered || !isAbortHandle(value)) {
42
+ throw new Error("Invalid Code Mode cancellation registration");
43
+ }
44
+ this.#registered = true;
45
+ this.#handle = value.dup();
46
+ if (this.#requested) {
47
+ this.abort();
48
+ await this.#aborting;
49
+ }
50
+ }
51
+
52
+ abort(): void {
53
+ this.#requested = true;
54
+ const handle = this.#handle;
55
+ if (!handle || this.#aborting) return;
56
+ this.#aborting = Promise.resolve().then(() => handle.abort()).then(
57
+ () => undefined,
58
+ (error: unknown) => {
59
+ if (!(error instanceof Error && error.message === this.abortMessage)) {
60
+ this.failure = error;
61
+ }
62
+ },
63
+ );
64
+ }
65
+
66
+ async close(): Promise<void> {
67
+ this.abort();
68
+ await this.#aborting;
69
+ try {
70
+ this.#handle?.[Symbol.dispose]();
71
+ } catch (error) {
72
+ this.failure = error;
73
+ } finally {
74
+ this.#handle = undefined;
75
+ }
76
+ }
77
+ }
78
+
79
+ class GuardedConnector extends RpcTarget {
80
+ constructor(
81
+ readonly binding: ConnectorBinding["binding"],
82
+ readonly check: () => void,
83
+ ) {
84
+ super();
85
+ }
86
+
87
+ async callTool(method: string, args: unknown): Promise<unknown> {
88
+ this.check();
89
+ return await this.binding.callTool(method, args);
90
+ }
91
+ }
92
+
93
+ function setup(abortMessage: string): string {
94
+ return `await (async () => {
95
+ const { RpcTarget } = await import("cloudflare:workers");
96
+ const context = this.ctx;
97
+ if (typeof context.abort !== "function") throw new Error("Native Code Mode cancellation is unavailable");
98
+ const abort = context.abort.bind(context);
99
+ const reason = new Error(${JSON.stringify(abortMessage)});
100
+ class Cancellation extends RpcTarget {
101
+ abort() { abort(reason); }
102
+ }
103
+ await ${CONTROL}.register(new Cancellation());
104
+ })();`;
105
+ }
106
+
107
+ /** Cancels a native sandbox pass, not its Host effects or the loaded Worker isolate. */
108
+ export function createCodeExecutionExecutor(
109
+ options: DynamicWorkerExecutorOptions,
110
+ signal?: AbortSignal,
111
+ ): Executor {
112
+ const executor = new DynamicWorkerExecutor(options);
113
+ return {
114
+ async execute(code, providersOrFns, executeOptions): Promise<ExecuteResult> {
115
+ if (signal?.aborted) return { result: undefined, error: CANCELLED };
116
+ const providers: ResolvedProvider[] = Array.isArray(providersOrFns)
117
+ ? providersOrFns
118
+ : [{ name: "codemode", fns: providersOrFns }];
119
+ const connectors = executeOptions?.connectors ?? [];
120
+ if ([...providers, ...connectors].some(({ name }) => name === CONTROL || name === SETUP)) {
121
+ return { result: undefined, error: "Code execution uses a reserved namespace" };
122
+ }
123
+ const control = new ExecutionControl(`Code Mode pass aborted (${crypto.randomUUID()})`);
124
+ let ended = false;
125
+ const check = () => {
126
+ if (signal?.aborted || ended) throw new Error(CANCELLED);
127
+ };
128
+ const onAbort = () => control.abort();
129
+ signal?.addEventListener("abort", onAbort, { once: true });
130
+ let result: ExecuteResult;
131
+ try {
132
+ if (signal?.aborted) return { result: undefined, error: CANCELLED };
133
+ result = await executor.execute(code, [{
134
+ name: SETUP,
135
+ fns: {},
136
+ prelude: setup(control.abortMessage),
137
+ }, ...providers.map((provider) => ({
138
+ ...provider,
139
+ fns: Object.fromEntries(Object.entries(provider.fns).map(([name, fn]) => [
140
+ name,
141
+ async (...args: unknown[]) => {
142
+ check();
143
+ return await fn(...args);
144
+ },
145
+ ])),
146
+ }))], {
147
+ ...executeOptions,
148
+ connectors: [{
149
+ name: CONTROL,
150
+ binding: control,
151
+ }, ...connectors.map((connector) => ({
152
+ ...connector,
153
+ binding: new GuardedConnector(connector.binding, check),
154
+ }))],
155
+ });
156
+ } catch (error) {
157
+ result = {
158
+ result: undefined,
159
+ error: error instanceof Error && error.message === control.abortMessage
160
+ ? CANCELLED
161
+ : error instanceof Error ? error.message : String(error),
162
+ };
163
+ } finally {
164
+ ended = true;
165
+ signal?.removeEventListener("abort", onAbort);
166
+ // A returned SDK timeout is only a soft race; stop the native pass too.
167
+ await control.close();
168
+ }
169
+ if (control.failure) return { result: undefined, error: "Failed to stop the Code Mode pass" };
170
+ return signal?.aborted ? { result: undefined, error: CANCELLED } : result;
171
+ },
172
+ };
173
+ }
@@ -1,3 +1,4 @@
1
+ import { truncateResult } from "@cloudflare/codemode";
1
2
  import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
2
3
  import { Type } from "@earendil-works/pi-ai";
3
4
  import type { RuntimeCodeExecutionPort } from "../../config/ports";
@@ -86,17 +87,18 @@ function browserResult(details: unknown): AgentToolResult<unknown> {
86
87
  }
87
88
 
88
89
  const image = record(inner?.image);
90
+ const nativeImage = inner?.type === "browser_screenshot" ? inner : undefined;
89
91
  const screenshotArgs = record(screenshotCall?.args);
90
92
  const screenshotResult = record(screenshotCall?.result);
91
93
  const format = record(screenshotArgs?.params)?.format;
92
94
  const data = typeof image?.data === "string"
93
95
  ? image.data
94
- : screenshotResult?.data;
96
+ : nativeImage ? nativeImage.data : screenshotResult?.data;
95
97
  const mimeType = typeof image?.mimeType === "string"
96
98
  ? image.mimeType
97
- : screenshotMimeType(format);
98
- if (!(inner && "image" in inner) && typeof data !== "string") {
99
- return result(details);
99
+ : nativeImage ? nativeImage.mediaType : screenshotMimeType(format);
100
+ if (!(inner && "image" in inner) && !nativeImage && typeof data !== "string") {
101
+ return result("result" in outer ? { ...outer, result: truncateResult(rawResult) } : details);
100
102
  }
101
103
  const projectedImage = browserImageMetadata(data, mimeType);
102
104
 
@@ -127,10 +129,9 @@ function browserResult(details: unknown): AgentToolResult<unknown> {
127
129
  const projectedDetails = {
128
130
  ...outer,
129
131
  result: inner
130
- ? {
131
- ...inner,
132
- image: projectedImage,
133
- }
132
+ ? nativeImage
133
+ ? { ...inner, data: projectedImage }
134
+ : { ...inner, image: projectedImage }
134
135
  : {
135
136
  image: projectedImage,
136
137
  note: "Screenshot recovered from the Page.captureScreenshot result.",
@@ -172,30 +173,18 @@ function runCodemode(
172
173
  ): AgentTool<typeof executeParameters>["execute"] {
173
174
  return async (_toolCallId, input, signal) => {
174
175
  signal?.throwIfAborted();
175
- // ponytail: 外层截止只保证 Agent 继续;Codemode 支持 AbortSignal 或宿主 dispose 后再终止底层执行。
176
- let timeout: ReturnType<typeof setTimeout> | undefined;
177
- const deadline = new Promise<{
178
- status: "error";
179
- code: "timeout";
180
- error: string;
181
- retryable: false;
182
- outcome: "unknown";
183
- }>((resolve) => {
184
- timeout = setTimeout(() => resolve({
185
- status: "error",
186
- code: "timeout",
187
- error:
188
- `${label} exceeded ${CODEMODE_EXECUTE_TIMEOUT_MS} ms; ` +
189
- "use existing results and state the missing evidence.",
190
- retryable: false,
191
- outcome: "unknown",
192
- }), CODEMODE_EXECUTE_TIMEOUT_MS);
193
- });
176
+ const execution = new AbortController();
177
+ const cancel = () => execution.abort(signal?.reason);
178
+ signal?.addEventListener("abort", cancel, { once: true });
179
+ // The first abort reason distinguishes a parent cancellation from our deadline.
180
+ const timeout = setTimeout(() => execution.abort(new Error(
181
+ `${label} exceeded ${CODEMODE_EXECUTE_TIMEOUT_MS} ms; ` +
182
+ "use existing results and state the missing evidence.",
183
+ )), CODEMODE_EXECUTE_TIMEOUT_MS);
194
184
  try {
195
- const output = await Promise.race([
196
- runtime.execute(input),
197
- deadline.then(result),
198
- ]);
185
+ // The Port settles only after the executor and connector cleanup finish.
186
+ const output = await runtime.execute(input, { signal: execution.signal });
187
+ execution.signal.throwIfAborted();
199
188
  const details = output.details as { status?: unknown; error?: unknown } | null;
200
189
  if (details?.status === "error") {
201
190
  throw new Error(typeof details.error === "string"
@@ -203,8 +192,12 @@ function runCodemode(
203
192
  : `${label} failed`);
204
193
  }
205
194
  return project(output);
195
+ } catch (error) {
196
+ execution.signal.throwIfAborted();
197
+ throw error;
206
198
  } finally {
207
- if (timeout !== undefined) clearTimeout(timeout);
199
+ clearTimeout(timeout);
200
+ signal?.removeEventListener("abort", cancel);
208
201
  }
209
202
  };
210
203
  }