@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/cli.js +3336 -3318
  3. package/dist/types/advisor/advise-tool.d.ts +12 -1
  4. package/dist/types/advisor/runtime.d.ts +41 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/update-cli.d.ts +4 -1
  7. package/dist/types/cli/usage-cli.d.ts +3 -0
  8. package/dist/types/cli/usage-error.d.ts +4 -0
  9. package/dist/types/config/api-key-resolver.d.ts +2 -2
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/model-resolver.d.ts +8 -1
  12. package/dist/types/config/models-config.d.ts +1 -1
  13. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
  14. package/dist/types/eval/bridge-timeout.d.ts +9 -1
  15. package/dist/types/eval/js/context-manager.d.ts +5 -3
  16. package/dist/types/eval/js/process-entry.d.ts +6 -0
  17. package/dist/types/eval/js/worker-core.d.ts +15 -1
  18. package/dist/types/eval/py/spawn-options.d.ts +10 -0
  19. package/dist/types/eval/py/tool-bridge.d.ts +1 -0
  20. package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  22. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  23. package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
  24. package/dist/types/main.d.ts +1 -0
  25. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  26. package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
  27. package/dist/types/modes/orchestrate.d.ts +1 -1
  28. package/dist/types/modes/rpc/host-tools.d.ts +2 -0
  29. package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
  30. package/dist/types/modes/ultrathink.d.ts +1 -1
  31. package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
  32. package/dist/types/modes/workflow.d.ts +1 -1
  33. package/dist/types/session/agent-session.d.ts +6 -0
  34. package/dist/types/session/exit-diagnostics.d.ts +11 -0
  35. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
  36. package/dist/types/subprocess/worker-client.d.ts +6 -0
  37. package/dist/types/tools/bash-skill-urls.d.ts +1 -0
  38. package/dist/types/web/search/provider.d.ts +10 -3
  39. package/dist/types/web/search/providers/codex.d.ts +5 -4
  40. package/package.json +12 -12
  41. package/src/advisor/__tests__/advisor.test.ts +830 -42
  42. package/src/advisor/advise-tool.ts +17 -1
  43. package/src/advisor/runtime.ts +288 -67
  44. package/src/autolearn/controller.ts +15 -3
  45. package/src/cli/args.ts +12 -0
  46. package/src/cli/auth-broker-cli.ts +30 -11
  47. package/src/cli/auth-gateway-cli.ts +5 -1
  48. package/src/cli/dry-balance-cli.ts +14 -4
  49. package/src/cli/flag-tables.ts +21 -7
  50. package/src/cli/update-cli.ts +62 -11
  51. package/src/cli/usage-cli.ts +58 -5
  52. package/src/cli/usage-error.ts +7 -0
  53. package/src/cli.ts +23 -1
  54. package/src/commands/acp.ts +11 -2
  55. package/src/commands/launch.ts +12 -3
  56. package/src/commands/token.ts +3 -1
  57. package/src/config/api-key-resolver.ts +12 -3
  58. package/src/config/config-file.ts +30 -12
  59. package/src/config/model-registry.ts +7 -7
  60. package/src/config/model-resolver.ts +21 -7
  61. package/src/config/models-config.ts +1 -1
  62. package/src/eval/__tests__/agent-bridge.test.ts +19 -14
  63. package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
  64. package/src/eval/__tests__/js-context-manager.test.ts +158 -1
  65. package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
  66. package/src/eval/__tests__/process-entry-import.test.ts +27 -0
  67. package/src/eval/agent-bridge.ts +121 -116
  68. package/src/eval/bridge-timeout.ts +20 -2
  69. package/src/eval/executor-base.ts +85 -7
  70. package/src/eval/jl/kernel.ts +2 -1
  71. package/src/eval/js/context-manager.ts +109 -32
  72. package/src/eval/js/process-entry.ts +27 -0
  73. package/src/eval/js/shared/runtime.ts +1 -1
  74. package/src/eval/js/worker-core.ts +70 -9
  75. package/src/eval/js/worker-entry.ts +1 -1
  76. package/src/eval/py/kernel.ts +2 -1
  77. package/src/eval/py/spawn-options.ts +13 -0
  78. package/src/eval/py/tool-bridge.ts +13 -14
  79. package/src/eval/rb/kernel.ts +2 -1
  80. package/src/extensibility/custom-tools/types.ts +3 -0
  81. package/src/extensibility/extensions/runner.ts +3 -0
  82. package/src/extensibility/extensions/types.ts +3 -0
  83. package/src/extensibility/plugins/manager.ts +21 -0
  84. package/src/internal-urls/memory-protocol.ts +13 -9
  85. package/src/lsp/client.ts +7 -1
  86. package/src/main.ts +29 -0
  87. package/src/mcp/tool-bridge.ts +57 -6
  88. package/src/modes/components/chat-transcript-builder.ts +22 -1
  89. package/src/modes/components/status-line/component.ts +10 -1
  90. package/src/modes/components/transcript-container.ts +110 -7
  91. package/src/modes/controllers/command-controller.ts +12 -4
  92. package/src/modes/controllers/event-controller.ts +80 -15
  93. package/src/modes/controllers/selector-controller.ts +15 -3
  94. package/src/modes/magic-keyword-boundary.ts +23 -0
  95. package/src/modes/orchestrate.ts +6 -5
  96. package/src/modes/print-mode.ts +9 -0
  97. package/src/modes/rpc/host-tools.ts +15 -0
  98. package/src/modes/rpc/rpc-mode.ts +123 -48
  99. package/src/modes/ultrathink.ts +6 -5
  100. package/src/modes/utils/transcript-render-helpers.ts +54 -0
  101. package/src/modes/utils/ui-helpers.ts +27 -1
  102. package/src/modes/workflow.ts +6 -5
  103. package/src/prompts/advisor/system.md +1 -0
  104. package/src/sdk.ts +33 -3
  105. package/src/session/agent-session.ts +239 -17
  106. package/src/session/exit-diagnostics.ts +108 -0
  107. package/src/session/streaming-output.ts +40 -12
  108. package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
  109. package/src/slash-commands/helpers/logout.ts +23 -3
  110. package/src/slash-commands/helpers/usage-report.ts +14 -2
  111. package/src/subprocess/worker-client.ts +9 -2
  112. package/src/task/executor.ts +8 -0
  113. package/src/task/render.test.ts +36 -0
  114. package/src/task/render.ts +55 -43
  115. package/src/tools/bash-skill-urls.ts +4 -1
  116. package/src/tools/bash.ts +1 -0
  117. package/src/tools/write.ts +82 -9
  118. package/src/tools/yield.ts +29 -1
  119. package/src/web/search/index.ts +39 -22
  120. package/src/web/search/provider.ts +33 -16
  121. package/src/web/search/providers/codex.ts +68 -21
@@ -449,130 +449,135 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
449
449
  // runtime is parked waiting for the result, and the cell timeout must
450
450
  // not abort us mid-cherry-pick or mid-nested-commit. The clock restarts
451
451
  // only after we hand control back to the runtime.
452
- const { result, mergeSummary, changesApplied } = await withBridgeTimeoutPause(options.emitStatus, async () => {
453
- let isolationContext: IsolationContext | null = null;
454
- if (isIsolated) {
455
- try {
456
- isolationContext = await prepareIsolationContext(options.session.cwd);
457
- } catch (err) {
458
- const message = err instanceof Error ? err.message : String(err);
459
- throw new ToolError(`Isolated agent() execution requires a git repository. ${message}`);
460
- }
461
- }
462
- const preferredBackend = isIsolated ? parseIsolationMode(isolationMode) : undefined;
463
-
464
- const result = await (async () => {
465
- if (!isolationContext) {
466
- return taskExecutor.runSubprocess(baseRunOptions);
467
- }
468
- const taskStart = Date.now();
469
- return runIsolatedSubprocess({
470
- baseOptions: baseRunOptions,
471
- context: isolationContext,
472
- preferredBackend,
473
- agentId: id,
474
- mergeMode,
475
- artifactsDir,
476
- description: trimToUndefined(parsed.label),
477
- buildCommitMessage,
478
- buildFailureResult: err => {
452
+ const { result, mergeSummary, changesApplied } = await withBridgeTimeoutPause(
453
+ options.emitStatus,
454
+ async () => {
455
+ let isolationContext: IsolationContext | null = null;
456
+ if (isIsolated) {
457
+ try {
458
+ isolationContext = await prepareIsolationContext(options.session.cwd);
459
+ } catch (err) {
479
460
  const message = err instanceof Error ? err.message : String(err);
480
- return {
481
- index: 0,
482
- id,
483
- agent: effectiveAgent.name,
484
- agentSource: effectiveAgent.source,
485
- task: renderSubagentPrompt(assignment),
486
- assignment,
487
- description: trimToUndefined(parsed.label),
488
- exitCode: 1,
489
- output: "",
490
- stderr: message,
491
- truncated: false,
492
- durationMs: Date.now() - taskStart,
493
- tokens: 0,
494
- requests: 0,
495
- modelOverride,
496
- error: message,
497
- };
498
- },
499
- });
500
- })();
501
-
502
- if (result.exitCode !== 0 || result.error || result.aborted) {
503
- const failureMessage = buildSubagentFailureMessage(agentName, result);
504
- const recoveryHint = isIsolated ? await buildIsolationRecoveryHint(result, artifactsDir) : "";
505
- throw new ToolError(`${failureMessage}${recoveryHint}`);
506
- }
507
-
508
- let mergeSummary = "";
509
- let changesApplied: boolean | null = null;
510
- if (isIsolated && isolationContext) {
511
- if (applyChanges) {
512
- const outcome = await mergeIsolatedChanges({
513
- result,
514
- repoRoot: isolationContext.repoRoot,
515
- mergeMode,
516
- });
517
- mergeSummary = outcome.summary;
518
- changesApplied = outcome.changesApplied;
519
- if (outcome.changesApplied === false) {
520
- const summaryText = outcome.summary.trim();
521
- const recoveryHint = await buildIsolationRecoveryHint(result, artifactsDir);
522
- throw new ToolError(
523
- `agent() isolated apply failed for ${result.id}${summaryText ? `: ${summaryText}` : ""}${recoveryHint}`,
524
- );
461
+ throw new ToolError(`Isolated agent() execution requires a git repository. ${message}`);
525
462
  }
463
+ }
464
+ const preferredBackend = isIsolated ? parseIsolationMode(isolationMode) : undefined;
526
465
 
527
- const nestedSummary = await applyEligibleNestedPatches({
528
- result,
529
- repoRoot: isolationContext.repoRoot,
466
+ const result = await (async () => {
467
+ if (!isolationContext) {
468
+ return taskExecutor.runSubprocess(baseRunOptions);
469
+ }
470
+ const taskStart = Date.now();
471
+ return runIsolatedSubprocess({
472
+ baseOptions: baseRunOptions,
473
+ context: isolationContext,
474
+ preferredBackend,
475
+ agentId: id,
530
476
  mergeMode,
531
- changesApplied: outcome.changesApplied,
532
- mergedBranchForNestedPatches: outcome.mergedBranchForNestedPatches,
533
- commitMessage: buildCommitMessage(),
477
+ artifactsDir,
478
+ description: trimToUndefined(parsed.label),
479
+ buildCommitMessage,
480
+ buildFailureResult: err => {
481
+ const message = err instanceof Error ? err.message : String(err);
482
+ return {
483
+ index: 0,
484
+ id,
485
+ agent: effectiveAgent.name,
486
+ agentSource: effectiveAgent.source,
487
+ task: renderSubagentPrompt(assignment),
488
+ assignment,
489
+ description: trimToUndefined(parsed.label),
490
+ exitCode: 1,
491
+ output: "",
492
+ stderr: message,
493
+ truncated: false,
494
+ durationMs: Date.now() - taskStart,
495
+ tokens: 0,
496
+ requests: 0,
497
+ modelOverride,
498
+ error: message,
499
+ };
500
+ },
534
501
  });
535
- mergeSummary += nestedSummary;
536
- if (structured && nestedSummary.trim()) {
537
- const recoveryHint = await buildIsolationRecoveryHint(
538
- { ...result, patchPath: undefined, branchName: undefined },
539
- artifactsDir,
540
- );
541
- throw new ToolError(
542
- `agent() isolated nested patch apply failed for ${result.id}: ${plainIsolationSummary(nestedSummary)}${recoveryHint}`,
543
- );
544
- }
545
- } else if (result.branchName) {
546
- mergeSummary = `\n\nIsolation: changes captured on branch \`${result.branchName}\` (apply=false). Not merged.`;
547
- } else if (result.patchPath) {
548
- mergeSummary = `\n\nIsolation: changes captured at \`${result.patchPath}\` (apply=false). Not applied.`;
549
- } else {
550
- const nestedPatches = result.nestedPatches ?? [];
551
- if (nestedPatches.length > 0) {
552
- mergeSummary = `\n\nIsolation: changes captured for ${nestedPatches.length} nested repositor${nestedPatches.length === 1 ? "y" : "ies"} (apply=false). Not applied.`;
502
+ })();
503
+
504
+ if (result.exitCode !== 0 || result.error || result.aborted) {
505
+ const failureMessage = buildSubagentFailureMessage(agentName, result);
506
+ const recoveryHint = isIsolated ? await buildIsolationRecoveryHint(result, artifactsDir) : "";
507
+ throw new ToolError(`${failureMessage}${recoveryHint}`);
508
+ }
509
+
510
+ let mergeSummary = "";
511
+ let changesApplied: boolean | null = null;
512
+ if (isIsolated && isolationContext) {
513
+ if (applyChanges) {
514
+ const outcome = await mergeIsolatedChanges({
515
+ result,
516
+ repoRoot: isolationContext.repoRoot,
517
+ mergeMode,
518
+ });
519
+ mergeSummary = outcome.summary;
520
+ changesApplied = outcome.changesApplied;
521
+ if (outcome.changesApplied === false) {
522
+ const summaryText = outcome.summary.trim();
523
+ const recoveryHint = await buildIsolationRecoveryHint(result, artifactsDir);
524
+ throw new ToolError(
525
+ `agent() isolated apply failed for ${result.id}${summaryText ? `: ${summaryText}` : ""}${recoveryHint}`,
526
+ );
527
+ }
528
+
529
+ const nestedSummary = await applyEligibleNestedPatches({
530
+ result,
531
+ repoRoot: isolationContext.repoRoot,
532
+ mergeMode,
533
+ changesApplied: outcome.changesApplied,
534
+ mergedBranchForNestedPatches: outcome.mergedBranchForNestedPatches,
535
+ commitMessage: buildCommitMessage(),
536
+ });
537
+ mergeSummary += nestedSummary;
538
+ if (structured && nestedSummary.trim()) {
539
+ const recoveryHint = await buildIsolationRecoveryHint(
540
+ { ...result, patchPath: undefined, branchName: undefined },
541
+ artifactsDir,
542
+ );
543
+ throw new ToolError(
544
+ `agent() isolated nested patch apply failed for ${result.id}: ${plainIsolationSummary(nestedSummary)}${recoveryHint}`,
545
+ );
546
+ }
547
+ } else if (result.branchName) {
548
+ mergeSummary = `\n\nIsolation: changes captured on branch \`${result.branchName}\` (apply=false). Not merged.`;
549
+ } else if (result.patchPath) {
550
+ mergeSummary = `\n\nIsolation: changes captured at \`${result.patchPath}\` (apply=false). Not applied.`;
553
551
  } else {
554
- mergeSummary = "\n\nIsolation: no changes captured.";
552
+ const nestedPatches = result.nestedPatches ?? [];
553
+ if (nestedPatches.length > 0) {
554
+ mergeSummary = `\n\nIsolation: changes captured for ${nestedPatches.length} nested repositor${nestedPatches.length === 1 ? "y" : "ies"} (apply=false). Not applied.`;
555
+ } else {
556
+ mergeSummary = "\n\nIsolation: no changes captured.";
557
+ }
555
558
  }
556
559
  }
557
- }
558
-
559
- // Clean up the temp artifacts dir we created for this call only when the
560
- // caller will not need files from it later. Keep it when the runtime helper
561
- // will return an `agent://` handle (the `.md`/`.jsonl` backing files live
562
- // here) and on `apply=false` (`changesApplied === null`) where the caller
563
- // consumes `details.patchPath` / `details.branchName` /
564
- // `details.nestedPatches` out of band. Failed isolated applies throw
565
- // earlier with a recovery hint, so they never reach this gate.
566
- const shouldCleanupTempArtifacts = tempArtifactsDir && !parsed.handle && (!isIsolated || changesApplied === true);
567
- if (shouldCleanupTempArtifacts) {
568
- await fs.rm(artifactsDir, { recursive: true, force: true });
569
- unregisterArtifactsDir?.();
570
- }
571
-
572
- options.session.recordEvalSubagentUsage?.(result.usage?.output ?? 0);
573
-
574
- return { result, mergeSummary, changesApplied };
575
- });
560
+
561
+ // Clean up the temp artifacts dir we created for this call only when the
562
+ // caller will not need files from it later. Keep it when the runtime helper
563
+ // will return an `agent://` handle (the `.md`/`.jsonl` backing files live
564
+ // here) and on `apply=false` (`changesApplied === null`) where the caller
565
+ // consumes `details.patchPath` / `details.branchName` /
566
+ // `details.nestedPatches` out of band. Failed isolated applies throw
567
+ // earlier with a recovery hint, so they never reach this gate.
568
+ const shouldCleanupTempArtifacts =
569
+ tempArtifactsDir && !parsed.handle && (!isIsolated || changesApplied === true);
570
+ if (shouldCleanupTempArtifacts) {
571
+ await fs.rm(artifactsDir, { recursive: true, force: true });
572
+ unregisterArtifactsDir?.();
573
+ }
574
+
575
+ options.session.recordEvalSubagentUsage?.(result.usage?.output ?? 0);
576
+
577
+ return { result, mergeSummary, changesApplied };
578
+ },
579
+ { deferExternalAbort: true },
580
+ );
576
581
 
577
582
  return {
578
583
  text: structured ? result.output : result.output + mergeSummary,
@@ -26,6 +26,15 @@ export function isEvalTimeoutControlEvent(event: JsStatusEvent): boolean {
26
26
  return event.op === EVAL_TIMEOUT_PAUSE_OP || event.op === EVAL_TIMEOUT_RESUME_OP;
27
27
  }
28
28
 
29
+ /** Optional behavior for a timeout pause around a host bridge call. */
30
+ export interface BridgeTimeoutPauseOptions {
31
+ /**
32
+ * Marks the pause as an `agent()` call whose already-started work must finish
33
+ * before an external eval abort reaches the kernel.
34
+ */
35
+ deferExternalAbort?: boolean;
36
+ }
37
+
29
38
  /**
30
39
  * Run {@link operation} while suspending the eval watchdog through
31
40
  * {@link emitStatus}. A no-op wrapper when no status sink is wired.
@@ -33,12 +42,21 @@ export function isEvalTimeoutControlEvent(event: JsStatusEvent): boolean {
33
42
  export async function withBridgeTimeoutPause<T>(
34
43
  emitStatus: ((event: JsStatusEvent) => void) | undefined,
35
44
  operation: () => Promise<T>,
45
+ options?: BridgeTimeoutPauseOptions,
36
46
  ): Promise<T> {
37
47
  if (!emitStatus) return operation();
38
- emitStatus({ op: EVAL_TIMEOUT_PAUSE_OP });
48
+ emitStatus(
49
+ options?.deferExternalAbort
50
+ ? { op: EVAL_TIMEOUT_PAUSE_OP, deferExternalAbort: true }
51
+ : { op: EVAL_TIMEOUT_PAUSE_OP },
52
+ );
39
53
  try {
40
54
  return await operation();
41
55
  } finally {
42
- emitStatus({ op: EVAL_TIMEOUT_RESUME_OP });
56
+ emitStatus(
57
+ options?.deferExternalAbort
58
+ ? { op: EVAL_TIMEOUT_RESUME_OP, deferExternalAbort: true }
59
+ : { op: EVAL_TIMEOUT_RESUME_OP },
60
+ );
43
61
  }
44
62
  }
@@ -3,7 +3,7 @@ import { Settings } from "../config/settings";
3
3
  import { OutputSink } from "../session/streaming-output";
4
4
  import type { ToolSession } from "../tools";
5
5
  import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
6
- import { isEvalTimeoutControlEvent } from "./bridge-timeout";
6
+ import { EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP, isEvalTimeoutControlEvent } from "./bridge-timeout";
7
7
  import type { JsStatusEvent } from "./js/shared/types";
8
8
  import type { KernelDisplayOutput } from "./py/display";
9
9
  import { registerPyToolBridge } from "./py/tool-bridge";
@@ -158,6 +158,77 @@ export async function waitForPromiseWithCancellation<T>(
158
158
  return await resultPromise;
159
159
  }
160
160
 
161
+ interface BridgeAbortShield {
162
+ signal: AbortSignal | undefined;
163
+ abortRequested: boolean;
164
+ timedOut: boolean;
165
+ handleStatus?: (event: JsStatusEvent) => void;
166
+ dispose?: () => void;
167
+ }
168
+
169
+ function createBridgeAbortShield(source: AbortSignal | undefined): BridgeAbortShield {
170
+ const shield: BridgeAbortShield = {
171
+ signal: undefined,
172
+ abortRequested: false,
173
+ timedOut: false,
174
+ };
175
+ if (!source) return shield;
176
+
177
+ const controller = new AbortController();
178
+ let pauseDepth = 0;
179
+ let abortReason: unknown;
180
+ let removeAbortListener: (() => void) | undefined;
181
+
182
+ const requestAbort = (reason: unknown): void => {
183
+ shield.abortRequested = true;
184
+ shield.timedOut =
185
+ shield.timedOut ||
186
+ (typeof DOMException !== "undefined" && reason instanceof DOMException
187
+ ? reason.name === "TimeoutError"
188
+ : reason instanceof Error && reason.name === "TimeoutError");
189
+ abortReason = reason;
190
+ if (pauseDepth > 0 || controller.signal.aborted) return;
191
+ controller.abort(reason);
192
+ };
193
+
194
+ const onAbort = (): void => {
195
+ const reason = source.reason;
196
+ requestAbort(reason);
197
+ };
198
+
199
+ shield.signal = controller.signal;
200
+ shield.handleStatus = (event: JsStatusEvent): void => {
201
+ if (event.deferExternalAbort !== true) return;
202
+ if (event.op === EVAL_TIMEOUT_PAUSE_OP) {
203
+ pauseDepth++;
204
+ return;
205
+ }
206
+ if (event.op !== EVAL_TIMEOUT_RESUME_OP || pauseDepth === 0) return;
207
+ pauseDepth--;
208
+ if (shield.abortRequested && !controller.signal.aborted) controller.abort(abortReason);
209
+ };
210
+ shield.dispose = (): void => {
211
+ removeAbortListener?.();
212
+ removeAbortListener = undefined;
213
+ };
214
+
215
+ if (source.aborted) {
216
+ requestAbort(source.reason);
217
+ } else {
218
+ source.addEventListener("abort", onAbort, { once: true });
219
+ removeAbortListener = () => {
220
+ source.removeEventListener("abort", onAbort);
221
+ };
222
+ if (source.aborted) {
223
+ source.removeEventListener("abort", onAbort);
224
+ removeAbortListener = undefined;
225
+ requestAbort(source.reason);
226
+ }
227
+ }
228
+
229
+ return shield;
230
+ }
231
+
161
232
  export function createCancelledKernelResult(output: string): KernelExecutionResult {
162
233
  const outputBytes = Buffer.byteLength(output, "utf-8");
163
234
  const outputLines = output.length > 0 ? 1 : 0;
@@ -304,9 +375,11 @@ export async function executeWithKernelBase<
304
375
  const displayOutputs: KernelDisplayOutput[] = [];
305
376
  const deadlineMs = (resolveDeadlineMs ?? getExecutionDeadlineMs)(options);
306
377
  let executionTimeoutMs: number | undefined;
378
+ const abortShield = createBridgeAbortShield(options?.signal);
307
379
 
308
380
  const collectDisplay = (output: KernelDisplayOutput): void => {
309
381
  if (output.type === "status") {
382
+ abortShield.handleStatus?.(output.event);
310
383
  options?.onStatus?.(output.event);
311
384
  if (!isJulia && isEvalTimeoutControlEvent(output.event)) return;
312
385
  }
@@ -320,8 +393,11 @@ export async function executeWithKernelBase<
320
393
  options?.toolSession && options?.bridgeSessionId
321
394
  ? registerPyToolBridge(options.bridgeSessionId, runId, {
322
395
  toolSession: options.toolSession,
323
- signal: options.signal,
396
+ signal: abortShield.signal,
324
397
  emitStatus,
398
+ abortRequested: () => {
399
+ return abortShield.abortRequested;
400
+ },
325
401
  })
326
402
  : null;
327
403
 
@@ -338,14 +414,15 @@ export async function executeWithKernelBase<
338
414
  cwd: options?.cwd,
339
415
  env: buildKernelEnvPatch(options ?? ({} as TOptions)),
340
416
  id: runId,
341
- signal: options?.signal,
417
+ signal: abortShield.signal,
342
418
  timeoutMs: executionTimeoutMs,
343
419
  onChunk: text => sink.push(text),
344
420
  onDisplay: output => collectDisplay(output),
345
421
  });
346
422
 
347
- if (result.cancelled) {
348
- const annotation = result.timedOut
423
+ if (result.cancelled || abortShield.abortRequested) {
424
+ const timedOut = result.timedOut || abortShield.timedOut;
425
+ const annotation = timedOut
349
426
  ? formatKernelTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs, result.kernelKilled ?? false)
350
427
  : undefined;
351
428
  const dumped = await sink.dump(annotation);
@@ -397,8 +474,8 @@ export async function executeWithKernelBase<
397
474
  stdinRequested: false,
398
475
  };
399
476
  } catch (err) {
400
- if (isCancellationError(err, cancelledErrorClass) || options?.signal?.aborted) {
401
- const timedOut = isTimedOutCancellation(err, cancelledErrorClass, options?.signal);
477
+ if (isCancellationError(err, cancelledErrorClass) || abortShield.abortRequested || abortShield.signal?.aborted) {
478
+ const timedOut = abortShield.timedOut || isTimedOutCancellation(err, cancelledErrorClass, abortShield.signal);
402
479
  const dumped = await sink.dump(
403
480
  timedOut ? formatTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs) : undefined,
404
481
  );
@@ -421,5 +498,6 @@ export async function executeWithKernelBase<
421
498
  throw error;
422
499
  } finally {
423
500
  unregisterBridge?.();
501
+ abortShield.dispose?.();
424
502
  }
425
503
  }
@@ -13,7 +13,7 @@ import { $ } from "bun";
13
13
  import { Settings } from "../../config/settings";
14
14
  import { BaseKernel, getRemainingTimeMs, type KernelStartOptions } from "../kernel-base";
15
15
  import type { KernelDisplayOutput } from "../py/display";
16
- import { hostHasInheritableConsole, shouldHideKernelWindow } from "../py/spawn-options";
16
+ import { hostHasInheritableConsole, shouldDetachKernel, shouldHideKernelWindow } from "../py/spawn-options";
17
17
  import { JULIA_PRELUDE } from "./prelude";
18
18
  import RUNNER_SCRIPT from "./runner.jl" with { type: "text" };
19
19
  import {
@@ -187,6 +187,7 @@ export class JuliaKernel extends BaseKernel<KernelExecuteOptions> {
187
187
  [runtime.juliaPath, "--startup-file=no", "--history-file=no", "--color=no", "--project=@.", scriptPath],
188
188
  {
189
189
  cwd: options.cwd,
190
+ detached: shouldDetachKernel(process.platform),
190
191
  env: spawnEnv,
191
192
  stdin: "pipe",
192
193
  stdout: "pipe",