@code-yeongyu/senpi-codemode 2026.10.10 → 2026.10.12

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 (110) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/README.md +113 -11
  3. package/package.json +7 -6
  4. package/src/bridge/memory-protocol.ts +8 -1
  5. package/src/bridge/protocol.ts +9 -4
  6. package/src/bridge/reserved.ts +2 -0
  7. package/src/bridges/eval-environment-schemas.ts +52 -0
  8. package/src/bridges/eval-virtual-schemas.ts +11 -4
  9. package/src/bridges/reserved-dispatch.ts +13 -1
  10. package/src/config/executable-settings.json +5 -0
  11. package/src/config/project-trust.ts +51 -0
  12. package/src/config/settings-overrides.ts +2 -0
  13. package/src/environments/editable-check.ts +1 -1
  14. package/src/environments/packages-install.ts +98 -0
  15. package/src/environments/py-environment.ts +7 -3
  16. package/src/environments/py-installer.ts +80 -44
  17. package/src/environments/python-environments.ts +1 -1
  18. package/src/extension/bridge-tool-call.ts +5 -1
  19. package/src/extension/eval-cells-rpc.ts +62 -0
  20. package/src/extension/eval-notifier.ts +13 -2
  21. package/src/extension/kernel-registration.ts +7 -2
  22. package/src/extension/runtime-factory.ts +42 -13
  23. package/src/extension/runtime-info.ts +6 -2
  24. package/src/extension/session-cwd.ts +25 -9
  25. package/src/extension/session-manager-contract.ts +3 -0
  26. package/src/extension/session-manager.ts +20 -6
  27. package/src/index.ts +8 -3
  28. package/src/interpreters/detect.ts +34 -1
  29. package/src/kernels/AGENTS.md +2 -0
  30. package/src/kernels/jl/globals.jl +128 -0
  31. package/src/kernels/jl/prelude.jl +15 -2
  32. package/src/kernels/jl/runner.jl +35 -143
  33. package/src/kernels/js/cell-ownership.js +268 -0
  34. package/src/kernels/js/cell-run-context.js +89 -0
  35. package/src/kernels/js/context-manager.ts +23 -24
  36. package/src/kernels/js/control-frames.ts +24 -0
  37. package/src/kernels/js/group-signal-notice.js +117 -0
  38. package/src/kernels/js/inline-worker.ts +2 -0
  39. package/src/kernels/js/kernel-contract.ts +16 -1
  40. package/src/kernels/js/kernel-memory-bridge.ts +1 -1
  41. package/src/kernels/js/kernel-memory.ts +79 -0
  42. package/src/kernels/js/kernel-tools-registry.js +1 -0
  43. package/src/kernels/js/local-module-loader.ts +8 -0
  44. package/src/kernels/js/process-drain-scan.d.ts +8 -0
  45. package/src/kernels/js/process-drain-scan.js +28 -0
  46. package/src/kernels/js/process-entry.js +466 -0
  47. package/src/kernels/js/process-memory-host.ts +83 -0
  48. package/src/kernels/js/process-tree-host.ts +4 -8
  49. package/src/kernels/js/process-tree.d.ts +11 -0
  50. package/src/kernels/js/process-tree.js +55 -10
  51. package/src/kernels/js/process-worker.ts +298 -0
  52. package/src/kernels/js/rejection-reports.js +61 -0
  53. package/src/kernels/js/run-queue.ts +7 -0
  54. package/src/kernels/js/windows-tree-kill-host.ts +117 -0
  55. package/src/kernels/js/windows-tree-kill.d.ts +19 -0
  56. package/src/kernels/js/windows-tree-kill.js +100 -0
  57. package/src/kernels/js/worker-core.js +51 -8
  58. package/src/kernels/js/worker-cwd.js +46 -12
  59. package/src/kernels/js/worker-host.ts +9 -2
  60. package/src/kernels/js/worker-indirect-eval.js +37 -13
  61. package/src/kernels/js/worker-require.d.ts +19 -0
  62. package/src/kernels/js/worker-require.js +52 -0
  63. package/src/kernels/js/worker-runtime.js +66 -13
  64. package/src/kernels/js/worker-shadow-guard.js +2 -6
  65. package/src/kernels/js/worker-shell-capture.js +9 -0
  66. package/src/kernels/js/worker-slot.ts +8 -0
  67. package/src/kernels/js/worker-startup.ts +49 -1
  68. package/src/kernels/js/worker-webview.js +15 -1
  69. package/src/kernels/py/prelude.py +69 -27
  70. package/src/kernels/py/process.ts +4 -0
  71. package/src/kernels/py/transport.ts +4 -0
  72. package/src/kernels/rb/runner.rb +9 -19
  73. package/src/kernels/sandbox/sandbox-cell.ts +26 -1
  74. package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
  75. package/src/kernels/shared/bun-ffi.ts +24 -0
  76. package/src/kernels/shared/kernel-memory-host.ts +30 -7
  77. package/src/kernels/shared/process-cpu.ts +7 -3
  78. package/src/kernels/shared/process-group-cpu.ts +6 -3
  79. package/src/kernels/shared/subprocess-kernel.ts +29 -3
  80. package/src/kernels/shared/subprocess-memory-globals.ts +101 -0
  81. package/src/kernels/shared/subprocess-process.ts +24 -2
  82. package/src/kernels/shared/subprocess-queue.ts +1 -0
  83. package/src/prompt/eval-prompt.ts +8 -1
  84. package/src/tool/AGENTS.md +1 -0
  85. package/src/tool/cell-handler.ts +3 -0
  86. package/src/tool/cell-reserved-dispatch.ts +1 -0
  87. package/src/tool/cell-runtime.ts +4 -2
  88. package/src/tool/detached-cell-contract.ts +3 -0
  89. package/src/tool/detached-cell-manager.ts +20 -11
  90. package/src/tool/detached-cell-notification.ts +11 -63
  91. package/src/tool/detached-cell-snapshot.ts +17 -0
  92. package/src/tool/detached-eval-result.ts +21 -10
  93. package/src/tool/detached-notification-queue.ts +1 -2
  94. package/src/tool/eval-tool.ts +4 -2
  95. package/src/tool/interrupt-note.ts +4 -5
  96. package/src/tool/live-headline.ts +60 -0
  97. package/src/tool/managed-cell.ts +0 -4
  98. package/src/tool/render-agent.ts +110 -0
  99. package/src/tool/render-blocks.ts +252 -0
  100. package/src/tool/render-call.ts +130 -0
  101. package/src/tool/render-cell.ts +165 -0
  102. package/src/tool/render-detail.ts +152 -0
  103. package/src/tool/render-live.ts +288 -0
  104. package/src/tool/render-result.ts +202 -0
  105. package/src/tool/render-status.ts +166 -0
  106. package/src/tool/render-tail.ts +139 -0
  107. package/src/tool/render.ts +3 -1069
  108. package/src/tool/run-eval-cell.ts +24 -14
  109. package/src/tool/runtime-label.ts +2 -1
  110. package/src/tool/types.ts +13 -2
@@ -13,7 +13,7 @@ import type {
13
13
  EvalDetachedCellSnapshot,
14
14
  EvalDetachedCellStatusEntry,
15
15
  } from "./detached-cell-contract.ts";
16
- import { currentDetachedResult, detachedErrorResult, snapshotDetachedCell } from "./detached-cell-snapshot.ts";
16
+ import { cancelledDetachedResult, detachedErrorResult, snapshotDetachedCell } from "./detached-cell-snapshot.ts";
17
17
  import {
18
18
  activeDetachedCellReuseError,
19
19
  allowsDetachedCellTransition,
@@ -35,11 +35,20 @@ export type {
35
35
  EvalDetachedCellStatusEntry,
36
36
  } from "./detached-cell-contract.ts";
37
37
 
38
+ export class EvalCellNotFoundError extends Error {
39
+ readonly code = "eval_cell_not_found";
40
+
41
+ constructor(cellId: string) {
42
+ super(`Unknown detached eval cell "${cellId}"`);
43
+ this.name = "EvalCellNotFoundError";
44
+ }
45
+ }
46
+
38
47
  export class EvalDetachedCellManager {
39
- readonly #artifactsDir: string | undefined;
40
48
  readonly #onStatusChange: ((entries: readonly EvalDetachedCellStatusEntry[]) => void) | undefined;
41
49
  readonly #onWakeSourceState: ((state: WakeSourceState) => void) | undefined;
42
50
  readonly #cells = new Map<string, ManagedCell>();
51
+ readonly #interrupting = new Map<string, ManagedCell>();
43
52
  readonly #detached = new Map<string, ManagedCell>();
44
53
  readonly #terminalSnapshots: TerminalSnapshotStore;
45
54
  readonly #notificationQueue: DetachedNotificationQueue;
@@ -49,7 +58,6 @@ export class EvalDetachedCellManager {
49
58
  readonly #maxDetachedCells: number;
50
59
 
51
60
  constructor(options: EvalDetachedCellManagerOptions = {}) {
52
- this.#artifactsDir = options.artifactsDir;
53
61
  this.#onStatusChange = options.onStatusChange;
54
62
  this.#onWakeSourceState = options.onWakeSourceState;
55
63
  this.#notificationQueue = new DetachedNotificationQueue(options.notifier);
@@ -85,7 +93,6 @@ export class EvalDetachedCellManager {
85
93
  const cell = createManagedCell({
86
94
  cellId,
87
95
  input,
88
- artifactsDir: this.#artifactsDir,
89
96
  now: this.#now,
90
97
  defaultHardLimitSeconds: this.#hardLimitSeconds,
91
98
  defaultRunBudgetSeconds: this.#runBudgetSeconds,
@@ -159,10 +166,11 @@ export class EvalDetachedCellManager {
159
166
  }
160
167
 
161
168
  async stop(cellId: string, reason = "Stopped detached eval cell"): Promise<EvalDetachedCellSnapshot> {
162
- const live = this.#cells.get(cellId);
169
+ const live = this.#cells.get(cellId) ?? this.#interrupting.get(cellId);
163
170
  if (live === undefined) return this.#terminal(cellId);
164
171
  if (live.state === "queued") this.#cancelQueued(live, reason);
165
- else if (live.detached) await this.#cancel(live, reason);
172
+ else if (detachedCellIsActive(live.state)) await this.#cancel(live, reason);
173
+ await live.interruptOutcome?.promise;
166
174
  return this.#snapshot(live);
167
175
  }
168
176
 
@@ -239,7 +247,6 @@ export class EvalDetachedCellManager {
239
247
  await cell.interruptOutcome?.promise;
240
248
  return this.#snapshot(cell);
241
249
  },
242
- spillPath: cell.spillPath,
243
250
  });
244
251
  }
245
252
  }
@@ -260,7 +267,7 @@ export class EvalDetachedCellManager {
260
267
  return;
261
268
  }
262
269
  if (foreground) {
263
- if (this.#settle(cell, "cancelled", currentDetachedResult(cell))) cell.onKill?.(expiry.error);
270
+ if (this.#settle(cell, "cancelled", cancelledDetachedResult(cell))) cell.onKill?.(expiry.error);
264
271
  return;
265
272
  }
266
273
  await this.#cancel(cell, expiry.error.message);
@@ -269,7 +276,7 @@ export class EvalDetachedCellManager {
269
276
  #cancelQueued(cell: ManagedCell, reason: string, error = new Error(reason)): void {
270
277
  const dequeued = cell.kernel?.cancelQueued(cell.cellId, reason) ?? false;
271
278
  cell.stateRetained = true;
272
- this.#settle(cell, "cancelled", currentDetachedResult(cell));
279
+ this.#settle(cell, "cancelled", cancelledDetachedResult(cell));
273
280
  // Acquisition/reset has no queue promise yet; its execution has no interrupt target either.
274
281
  if (!dequeued) cell.onKill?.(error);
275
282
  }
@@ -277,13 +284,15 @@ export class EvalDetachedCellManager {
277
284
  async #cancel(cell: ManagedCell, reason: string): Promise<void> {
278
285
  const outcome = Promise.withResolvers<void>();
279
286
  cell.interruptOutcome = outcome;
287
+ this.#interrupting.set(cell.cellId, cell);
280
288
  try {
281
- if (!this.#settle(cell, "cancelled", currentDetachedResult(cell)) || cell.kernel === undefined) return;
289
+ if (!this.#settle(cell, "cancelled", cancelledDetachedResult(cell)) || cell.kernel === undefined) return;
282
290
  const handle = await cell.kernel.interrupt(reason, cell.cellId);
283
291
  cell.interruptNote = handle.note;
284
292
  cell.stateRetained = await handle.stateRetained;
285
293
  } finally {
286
294
  outcome.resolve();
295
+ if (this.#interrupting.get(cell.cellId) === cell) this.#interrupting.delete(cell.cellId);
287
296
  this.#refreshTerminalSnapshot(cell);
288
297
  }
289
298
  }
@@ -308,7 +317,7 @@ export class EvalDetachedCellManager {
308
317
 
309
318
  #terminal(cellId: string): EvalDetachedCellSnapshot {
310
319
  const snapshot = this.#terminalSnapshots.get(cellId);
311
- if (snapshot === undefined) throw new Error(`Unknown detached eval cell "${cellId}"`);
320
+ if (snapshot === undefined) throw new EvalCellNotFoundError(cellId);
312
321
  return snapshot;
313
322
  }
314
323
  }
@@ -1,61 +1,21 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
1
  import type { EvalDetachedCellNotification, EvalDetachedCellSnapshot } from "./detached-cell-manager.ts";
4
2
  import { interruptionStateNote, unknownInterruptionStateNote } from "./interrupt-note.ts";
5
3
  import type { EvalKernelState, EvalMemoryDetails } from "./types.ts";
6
4
 
7
- const NOTIFICATION_TAIL_BYTES = 512;
8
-
9
- export function detachedNotificationSpillPath(artifactsDir: string | undefined, cellId: string): string | undefined {
10
- if (artifactsDir === undefined) return undefined;
11
- return join(artifactsDir, "local", `detached-eval-${safeCellId(cellId)}.log`);
12
- }
13
-
14
- export async function buildDetachedCellNotification(
15
- snapshot: EvalDetachedCellSnapshot,
16
- spillPath: string | undefined,
17
- ): Promise<EvalDetachedCellNotification> {
18
- const body = notificationBody(snapshot);
19
- const overflow = Buffer.byteLength(body, "utf8") > NOTIFICATION_TAIL_BYTES;
20
- let spillNotice = "";
21
- if (overflow && spillPath !== undefined) {
22
- try {
23
- await mkdir(dirname(spillPath), { recursive: true });
24
- await writeFile(spillPath, body, "utf8");
25
- // The agent read tool resolves plain paths only, so the notice must carry
26
- // the absolute spill path, never the kernel-helper local:// scheme.
27
- spillNotice = `\nBuffered output overflowed; full output: ${spillPath}`;
28
- } catch (error) {
29
- const message = error instanceof Error ? error.message : String(error);
30
- spillNotice = `\nBuffered output overflow could not be spilled: ${message}`;
31
- }
32
- }
33
- return {
34
- cellId: snapshot.cellId,
35
- content: `${overflow ? notificationPreview(snapshot) : body}${overflow ? "\n[…notification tail capped…]" : ""}${spillNotice}`,
36
- };
37
- }
38
-
39
- function notificationBody(cell: EvalDetachedCellSnapshot): string {
40
- const outcome = outcomeOf(cell);
41
- const resultText = textContent(cell);
42
- const stateNote = stateNoteOf(cell);
43
- return [
44
- `<system-reminder>Detached eval cell ${cell.cellId} (${cell.language}) ${outcome}.`,
45
- resultText.length === 0 ? "(no output)" : resultText,
46
- `${stateNote}</system-reminder>`,
47
- ].join("\n");
5
+ /**
6
+ * A detached cell's completion carries the same text its result would have shown in the foreground: the output sink
7
+ * already bounded it (the configured head, the tail, the middle marker and the artifact notice), plus its images.
8
+ */
9
+ export function buildDetachedCellNotification(snapshot: EvalDetachedCellSnapshot): EvalDetachedCellNotification {
10
+ const images = snapshot.result.content.filter((part) => part.type === "image");
11
+ return { cellId: snapshot.cellId, content: notificationText(snapshot, textContent(snapshot)), images };
48
12
  }
49
13
 
50
- function notificationPreview(cell: EvalDetachedCellSnapshot): string {
51
- const outcome = outcomeOf(cell);
52
- const resultText = textContent(cell);
53
- const stateNote = stateNoteOf(cell);
14
+ function notificationText(cell: EvalDetachedCellSnapshot, output: string): string {
54
15
  return [
55
- `<system-reminder>Detached eval cell ${cell.cellId} (${cell.language}) ${outcome}.`,
56
- "Buffered output tail:",
57
- truncateTailUtf8(resultText, NOTIFICATION_TAIL_BYTES),
58
- `${stateNote}</system-reminder>`,
16
+ `<system-reminder>Detached eval cell ${cell.cellId} (${cell.language}) ${outcomeOf(cell)}.`,
17
+ output.length === 0 ? "(no output)" : output,
18
+ `${stateNoteOf(cell)}</system-reminder>`,
59
19
  ].join("\n");
60
20
  }
61
21
 
@@ -101,15 +61,3 @@ export function memoryStateNote(memory: EvalMemoryDetails | undefined): string {
101
61
  return "The kernel was restarted before this cell ran - globals from earlier cells are gone; this cell's variables are available to the next eval cell.";
102
62
  return "Kernel state updated - variables are available to the next eval cell.";
103
63
  }
104
-
105
- function safeCellId(cellId: string): string {
106
- return cellId.replace(/[^a-zA-Z0-9_-]/gu, "_");
107
- }
108
-
109
- function truncateTailUtf8(text: string, maxBytes: number): string {
110
- if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
111
- const bytes = Buffer.from(text, "utf8");
112
- let start = bytes.length - maxBytes;
113
- while (start < bytes.length && (bytes[start] & 0xc0) === 0x80) start++;
114
- return bytes.subarray(start).toString("utf8");
115
- }
@@ -63,6 +63,23 @@ export function currentDetachedResult(cell: DetachedCellResultSource): AgentTool
63
63
  return cell.terminalResult ?? cell.liveResult?.() ?? fallbackResult(cell.input);
64
64
  }
65
65
 
66
+ /**
67
+ * The result a cell cancelled before its handler finished settles with: its buffered output, not the live progress
68
+ * frame ("1/1 cells running"), so a stopped cell never reads as still running.
69
+ */
70
+ export function cancelledDetachedResult(cell: DetachedCellResultSource): AgentToolResult<EvalToolDetails> {
71
+ if (cell.terminalResult !== undefined) return cell.terminalResult;
72
+ const current = currentDetachedResult(cell);
73
+ const output = detachedOutputTail(current);
74
+ return {
75
+ content: [
76
+ { type: "text", text: output.length > 0 ? output : "(no output)" },
77
+ ...current.content.filter((part) => part.type === "image"),
78
+ ],
79
+ details: current.details,
80
+ };
81
+ }
82
+
66
83
  export function detachedErrorResult(cell: DetachedCellResultSource, error: Error): AgentToolResult<EvalToolDetails> {
67
84
  const current = currentDetachedResult(cell);
68
85
  const output = detachedOutputTail(current);
@@ -36,22 +36,25 @@ export async function executeEvalControl(
36
36
  return createDetachedControlResult(snapshot);
37
37
  }
38
38
 
39
- function createEvalListResult(cellManager: EvalDetachedCellManager): AgentToolResult<EvalListDetails> {
39
+ export function listEvalCells(cellManager: EvalDetachedCellManager): EvalListedCell[] {
40
40
  const { live, recent } = cellManager.list();
41
- const snapshots = [...live, ...recent];
42
- const cells: EvalListedCell[] = snapshots.map((snapshot) => {
41
+ return [...live, ...recent].map((snapshot) => {
43
42
  const summary = snapshot.result.details.summary;
44
43
  return {
45
44
  cellId: snapshot.cellId,
46
45
  language: snapshot.language,
47
46
  state: snapshot.state,
48
47
  startedAtMs: snapshot.startedAtMs,
49
- ...(snapshot.queuedBehind === undefined || snapshot.queuedBehind.length === 0
50
- ? {}
51
- : { queuedBehind: [...snapshot.queuedBehind] }),
48
+ ...(snapshot.queuedBehind === undefined ? {} : { queuedBehind: [...snapshot.queuedBehind] }),
52
49
  ...(summary ? { summary } : {}),
53
50
  };
54
51
  });
52
+ }
53
+
54
+ function createEvalListResult(cellManager: EvalDetachedCellManager): AgentToolResult<EvalListDetails> {
55
+ const { live, recent } = cellManager.list();
56
+ const snapshots = [...live, ...recent];
57
+ const cells = listEvalCells(cellManager);
55
58
  const text = snapshots
56
59
  .map((snapshot, index) => {
57
60
  const cell = cells[index];
@@ -60,7 +63,7 @@ function createEvalListResult(cellManager: EvalDetachedCellManager): AgentToolRe
60
63
  " ",
61
64
  );
62
65
  const elapsed = Math.floor(snapshot.result.details.durationMs / 1000);
63
- const queued = cell.queuedBehind === undefined ? "" : ` queued behind ${cell.queuedBehind.join(", ")}`;
66
+ const queued = cell.queuedBehind === undefined ? "" : ` ${queuedPhrase(cell.queuedBehind, cell.language)}`;
64
67
  return `${cell.cellId} ${cell.language} ${cell.state} ${elapsed}s${queued} - ${preview}`;
65
68
  })
66
69
  .join("\n");
@@ -76,11 +79,13 @@ export function resultAfterDetach(
76
79
  otherLiveCells: number,
77
80
  ): AgentToolResult<EvalToolDetails> {
78
81
  if (snapshot.state !== "detached" && snapshot.state !== "running") return createDetachedControlResult(snapshot);
79
- const predecessors = snapshot.queuedBehind?.join(", ");
82
+ const queuedBehind = snapshot.queuedBehind;
80
83
  const text =
81
- predecessors === undefined
84
+ queuedBehind === undefined
82
85
  ? `Eval cell ${snapshot.cellId} detached and is running in the ${input.language} kernel (${otherLiveCells} other live cells). Completion arrives as a notification; do not re-run it. eval({ action: "peek" | "stop", cell_id }) or eval({ action: "list" }).`
83
- : `Eval cell ${snapshot.cellId} is queued behind ${predecessors} in the ${input.language} kernel and detached; it runs after ${predecessors} and completes as one notification. peek/stop/list with eval({ action, cell_id })`;
86
+ : queuedBehind.length === 0
87
+ ? `Eval cell ${snapshot.cellId} is detached and waiting for the ${input.language} kernel to be ready; it runs first once the kernel is ready and completes as one notification. peek/stop/list with eval({ action, cell_id })`
88
+ : `Eval cell ${snapshot.cellId} is queued behind ${queuedBehind.join(", ")} in the ${input.language} kernel and detached; it runs after ${queuedBehind.join(", ")} and completes as one notification. peek/stop/list with eval({ action, cell_id })`;
84
89
  return {
85
90
  content: [
86
91
  {
@@ -164,6 +169,12 @@ export function resultForDetachedState(
164
169
  };
165
170
  }
166
171
 
172
+ function queuedPhrase(queuedBehind: readonly string[], language: string): string {
173
+ return queuedBehind.length === 0
174
+ ? `waiting for the ${language} kernel to be ready`
175
+ : `queued behind ${queuedBehind.join(", ")}`;
176
+ }
177
+
167
178
  function textContent(result: AgentToolResult<EvalToolDetails>): string {
168
179
  return result.content
169
180
  .filter((part) => part.type === "text")
@@ -3,7 +3,6 @@ import { buildDetachedCellNotification } from "./detached-cell-notification.ts";
3
3
 
4
4
  export interface PendingDetachedNotification {
5
5
  readonly snapshot: () => EvalDetachedCellSnapshot | Promise<EvalDetachedCellSnapshot>;
6
- readonly spillPath: string | undefined;
7
6
  }
8
7
 
9
8
  export class DetachedNotificationQueue {
@@ -30,7 +29,7 @@ export class DetachedNotificationQueue {
30
29
  const flush = Promise.resolve().then(async () => {
31
30
  const pending = this.#pending.splice(0);
32
31
  const notifications = await Promise.all(
33
- pending.map(async (item) => await buildDetachedCellNotification(await item.snapshot(), item.spillPath)),
32
+ pending.map(async (item) => buildDetachedCellNotification(await item.snapshot())),
34
33
  );
35
34
  this.#notifier?.notify(notifications);
36
35
  });
@@ -1,6 +1,6 @@
1
1
  import type { AgentToolResult, ToolDefinition } from "@code-yeongyu/senpi";
2
2
  import type { TUnsafe } from "typebox";
3
- import { resolveSandbox } from "../config/feature-settings.ts";
3
+ import { resolveAdvertiseHelpers, resolveSandbox } from "../config/feature-settings.ts";
4
4
  import {
5
5
  DEFAULT_FOREGROUND_WINDOW_SECONDS,
6
6
  defaultCodemodeSettings,
@@ -25,6 +25,7 @@ import {
25
25
  type EvalListInput,
26
26
  type EvalResultDetails,
27
27
  type EvalToolDetails,
28
+ type EvalToolInput,
28
29
  type EvalToolRequest,
29
30
  enabledLanguageList,
30
31
  } from "./types.ts";
@@ -34,7 +35,7 @@ export type { CreateEvalToolOptions } from "./eval-tool-options.ts";
34
35
  export type { EnabledEvalLanguages, EvalKernel, EvalKernelManager } from "./types.ts";
35
36
 
36
37
  type EvalExecuteArgs<Request extends EvalToolRequest> = Parameters<
37
- ToolDefinition<TUnsafe<Request>, EvalToolDetails>["execute"]
38
+ ToolDefinition<TUnsafe<Request | Partial<EvalToolInput>>, EvalToolDetails>["execute"]
38
39
  >;
39
40
 
40
41
  export function createEvalTool(options: CreateEvalToolOptions) {
@@ -53,6 +54,7 @@ export function createEvalTool(options: CreateEvalToolOptions) {
53
54
  const parameters = createEvalInputSchema(options.enabledLanguages, deadlines, { sandbox });
54
55
  const prompt = buildEvalPrompt(options.enabledLanguages, {
55
56
  spawns: options.spawns ?? false,
57
+ ...(resolveAdvertiseHelpers(options.settings ?? defaultCodemodeSettings) ? { advertiseHelpers: true } : {}),
56
58
  monitor: options.monitor,
57
59
  maxDetachedCells,
58
60
  runBudgetSeconds: deadlines.runBudgetSeconds,
@@ -26,9 +26,8 @@ export async function describeTimeoutState(
26
26
  );
27
27
  if (outcome === undefined) error.message = fallbackTimeoutMessage(error.message);
28
28
  else if (outcome.retained)
29
- error.message = `${error.message} The kernel remains running; its existing variables are preserved.`;
30
- else
31
- error.message = `${error.message} The kernel was unresponsive and restarted; variables from earlier cells are lost.`;
29
+ error.message = `${error.message} The kernel was not restarted; variables from earlier cells are kept.`;
30
+ else error.message = `${error.message} The kernel was restarted; variables from earlier cells are lost.`;
32
31
  if (outcome?.note !== undefined) error.message = `${error.message} ${outcome.note.trim()}`;
33
32
  return error;
34
33
  }
@@ -63,8 +62,8 @@ const LANGUAGE_LABEL: Record<EvalLanguage, string> = {
63
62
  export function interruptionStateNote(language: EvalLanguage, stateRetained: boolean | undefined): string | undefined {
64
63
  if (stateRetained === undefined) return undefined;
65
64
  const label = LANGUAGE_LABEL[language];
66
- if (stateRetained) return `${label} was interrupted and remains running; its existing variables are preserved.`;
67
- return `${label} was unresponsive to interrupt and was restarted; variables from earlier cells are lost.`;
65
+ if (stateRetained) return `The ${label} was not restarted; variables from earlier cells are kept.`;
66
+ return `The ${label} was restarted; variables from earlier cells are lost.`;
68
67
  }
69
68
 
70
69
  export function unknownInterruptionStateNote(language: EvalLanguage): string {
@@ -0,0 +1,60 @@
1
+ import { sanitizeTerminalLabel, visibleWidth } from "@code-yeongyu/senpi";
2
+ import { type EvalCellResult, type EvalLanguage, evalLanguageOrder } from "./types.ts";
3
+
4
+ const HEADLINED_STATUSES: ReadonlySet<EvalCellResult["status"]> = new Set(["pending", "queued", "running", "detached"]);
5
+
6
+ /**
7
+ * A collapsed headline never wraps the frame: it gets what the language, state and timing leave on the line. This
8
+ * floor (in screen cells) only keeps a word or two visible in very narrow terminals.
9
+ */
10
+ const MIN_HEADLINE_CELLS = 8;
11
+
12
+ export function leadsWithHeadline(status: EvalCellResult["status"]): boolean {
13
+ return HEADLINED_STATUSES.has(status);
14
+ }
15
+
16
+ /**
17
+ * What an in-progress cell is doing, in one line: its summary, or else the first line of its code that has content once sanitized (the
18
+ * oh-my-pi fallback), or an ellipsis while the arguments are still streaming in. Collapsed rows cut it to
19
+ * `maxCells` screen cells (a wide character takes two), so the headline stays one line. Whichever is chosen is
20
+ * sanitized before it is measured, so escape and control characters never reach the row (senpi#2831, senpi#2839).
21
+ */
22
+ export function liveHeadline(
23
+ summary: string | undefined,
24
+ code: string | undefined,
25
+ maxCells: number | undefined,
26
+ ): string {
27
+ const text = (summary === undefined ? "" : sanitizeTerminalLabel(summary)) || firstCodeLine(code) || "…";
28
+ const limit = maxCells === undefined ? undefined : Math.max(MIN_HEADLINE_CELLS, maxCells);
29
+ if (limit === undefined || visibleWidth(text) <= limit) return text;
30
+ return `${cellPrefix(text, limit - 1)}…`;
31
+ }
32
+
33
+ // A plain cut by screen cells: the headline is styled by the caller, so no reset codes may be inserted here.
34
+ function cellPrefix(text: string, cells: number): string {
35
+ let kept = "";
36
+ let used = 0;
37
+ for (const { segment } of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)) {
38
+ const width = visibleWidth(segment);
39
+ if (used + width > cells) break;
40
+ kept += segment;
41
+ used += width;
42
+ }
43
+ return kept;
44
+ }
45
+
46
+ export function knownLanguage(language: unknown): EvalLanguage | undefined {
47
+ return evalLanguageOrder.find((known) => known === language);
48
+ }
49
+
50
+ // The first line with something to show once it is sanitized: a line of only escape or control characters is skipped
51
+ // (senpi#2850), and the later lines never reach the row.
52
+ function firstCodeLine(code: string | undefined): string | undefined {
53
+ if (code === undefined) return undefined;
54
+ // Stops at the first line with content, so a long cell is not sanitized line by line on every frame.
55
+ for (const line of code.split("\n")) {
56
+ const clean = sanitizeTerminalLabel(line);
57
+ if (clean.length > 0) return clean;
58
+ }
59
+ return undefined;
60
+ }
@@ -1,7 +1,6 @@
1
1
  import type { AgentToolResult } from "@code-yeongyu/senpi";
2
2
  import { type CellDeadlineExpiry, CellDeadlines } from "./cell-deadlines.ts";
3
3
  import type { EvalDetachedCellSnapshot, EvalDetachedCellState } from "./detached-cell-contract.ts";
4
- import { detachedNotificationSpillPath } from "./detached-cell-notification.ts";
5
4
  import type { EvalKernel, EvalToolDetails, EvalToolInput } from "./types.ts";
6
5
 
7
6
  export type LiveResultProvider = () => AgentToolResult<EvalToolDetails>;
@@ -9,7 +8,6 @@ export type LiveResultProvider = () => AgentToolResult<EvalToolDetails>;
9
8
  export type ManagedCell = {
10
9
  readonly cellId: string;
11
10
  readonly input: EvalToolInput;
12
- readonly spillPath: string | undefined;
13
11
  readonly startedAtMs: number;
14
12
  readonly terminal: PromiseWithResolvers<EvalDetachedCellSnapshot>;
15
13
  state: Exclude<EvalDetachedCellState, "detached">;
@@ -37,7 +35,6 @@ export type ManagedCell = {
37
35
  export interface ManagedCellInit {
38
36
  readonly cellId: string;
39
37
  readonly input: EvalToolInput;
40
- readonly artifactsDir: string | undefined;
41
38
  readonly now: () => number;
42
39
  readonly defaultHardLimitSeconds: number;
43
40
  readonly defaultRunBudgetSeconds: number;
@@ -60,7 +57,6 @@ export function createManagedCell(init: ManagedCellInit): ManagedCell {
60
57
  return {
61
58
  cellId: init.cellId,
62
59
  input: init.input,
63
- spillPath: detachedNotificationSpillPath(init.artifactsDir, init.cellId),
64
60
  startedAtMs: init.now(),
65
61
  state: "queued",
66
62
  runStartedAtMs: undefined,
@@ -0,0 +1,110 @@
1
+ import {
2
+ appendLines,
3
+ assertNever,
4
+ type RenderEnvironment,
5
+ renderPrefixed,
6
+ type StatusPresentation,
7
+ spinner,
8
+ style,
9
+ } from "./render-blocks.ts";
10
+ import { eventString } from "./render-status.ts";
11
+ import { formatDuration } from "./tool-widgets.ts";
12
+ import type { EvalStatusEvent } from "./types.ts";
13
+
14
+ type AgentStatus = "pending" | "running" | "completed" | "failed" | "aborted";
15
+
16
+ function eventNumber(value: unknown): number {
17
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
18
+ }
19
+
20
+ function agentStatus(value: unknown): AgentStatus {
21
+ switch (value) {
22
+ case "pending":
23
+ case "running":
24
+ case "completed":
25
+ case "failed":
26
+ case "aborted":
27
+ return value;
28
+ default:
29
+ return "running";
30
+ }
31
+ }
32
+
33
+ function coalesceAgentEvents(events: readonly EvalStatusEvent[]): EvalStatusEvent[] {
34
+ const rows: EvalStatusEvent[] = [];
35
+ const indexes = new Map<string, number>();
36
+ for (const event of events) {
37
+ const id = eventString(event.id);
38
+ if (id === undefined) {
39
+ rows.push(event);
40
+ continue;
41
+ }
42
+ const index = indexes.get(id);
43
+ if (index === undefined) {
44
+ indexes.set(id, rows.length);
45
+ rows.push(event);
46
+ } else rows[index] = event;
47
+ }
48
+ return rows;
49
+ }
50
+
51
+ function agentPresentation(status: AgentStatus, spinnerFrame: number | undefined): StatusPresentation {
52
+ switch (status) {
53
+ case "pending":
54
+ return { label: "pending", icon: "○", color: "muted" };
55
+ case "running":
56
+ return { label: "running", icon: spinner(spinnerFrame), color: "warning" };
57
+ case "completed":
58
+ return { label: "done", icon: "✓", color: "success" };
59
+ case "failed":
60
+ return { label: "failed", icon: "✗", color: "error" };
61
+ case "aborted":
62
+ return { label: "aborted", icon: "×", color: "error" };
63
+ default:
64
+ return assertNever(status);
65
+ }
66
+ }
67
+
68
+ export function renderAgentProgressEvents(
69
+ events: readonly EvalStatusEvent[],
70
+ environment: RenderEnvironment,
71
+ ): string[] {
72
+ const rows = coalesceAgentEvents(events);
73
+ const lines: string[] = [];
74
+ // Senpi Theme has no tree-token API; fixed ├/└/│ glyphs intentionally mirror omp.
75
+ for (const [index, event] of rows.entries()) {
76
+ const isLast = index === rows.length - 1;
77
+ const status = agentStatus(event.status);
78
+ const presentation = agentPresentation(status, environment.spinnerFrame);
79
+ const id = eventString(event.id) ?? "agent";
80
+ const styledId = environment.theme === undefined ? id : environment.theme.bold(id);
81
+ let body = `${style(environment.theme, presentation.color, presentation.icon)} ${styledId} ${presentation.label}`;
82
+ if (status === "completed" || status === "failed" || status === "aborted") {
83
+ const duration = eventNumber(event.durationMs);
84
+ if (duration > 0) body += ` · ${style(environment.theme, "dim", formatDuration(duration))}`;
85
+ }
86
+ const branch = isLast ? "└ " : "├ ";
87
+ const continuation = isLast ? " " : "│ ";
88
+ appendLines(
89
+ lines,
90
+ renderPrefixed(body, environment, { prefix: branch, continuation: continuation, color: "dim" }),
91
+ );
92
+ if (status !== "running") continue;
93
+ const currentTool = eventString(event.currentTool);
94
+ const lastIntent = eventString(event.lastIntent);
95
+ if (currentTool === undefined && lastIntent === undefined) continue;
96
+ const detail =
97
+ currentTool === undefined
98
+ ? (lastIntent ?? "")
99
+ : `${currentTool}${lastIntent === undefined ? "" : `: ${lastIntent}`}`;
100
+ appendLines(
101
+ lines,
102
+ renderPrefixed(detail, environment, {
103
+ prefix: `${continuation}└ `,
104
+ continuation: `${continuation} `,
105
+ color: "dim",
106
+ }),
107
+ );
108
+ }
109
+ return lines;
110
+ }