@code-yeongyu/senpi-codemode 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 +57 -0
- package/README.md +20 -4
- package/package.json +4 -4
- package/src/bridge/reserved.ts +2 -0
- package/src/kernels/jl/kernel.ts +2 -2
- package/src/kernels/js/context-manager.ts +73 -129
- package/src/kernels/js/inline-worker.ts +2 -2
- package/src/kernels/js/interrupt-bounds.ts +66 -0
- package/src/kernels/js/run-queue.ts +18 -3
- package/src/kernels/js/worker-core.js +30 -0
- package/src/kernels/js/worker-runtime.js +16 -0
- package/src/kernels/js/worker-shell-capture.d.ts +9 -0
- package/src/kernels/js/worker-shell-capture.js +32 -6
- package/src/kernels/js/worker-slot.ts +106 -0
- package/src/kernels/js/worker-startup.ts +69 -0
- package/src/kernels/py/transport.ts +2 -2
- package/src/kernels/rb/kernel.ts +2 -2
- package/src/kernels/shared/runtime-asset.ts +40 -14
- package/src/prompt/eval-prompt.ts +10 -5
- package/src/tool/cell-execution.ts +9 -11
- package/src/tool/detached-cell-contract.ts +45 -0
- package/src/tool/detached-cell-manager.ts +43 -71
- package/src/tool/detached-cell-notification.ts +4 -5
- package/src/tool/detached-cell-snapshot.ts +2 -0
- package/src/tool/detached-cell-status.ts +30 -0
- package/src/tool/detached-eval-result.ts +1 -0
- package/src/tool/detached-notification-queue.ts +2 -2
- package/src/tool/interrupt-note.ts +29 -15
- package/src/tool/types.ts +2 -0
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@code-yeongyu/senpi";
|
|
2
2
|
import { DEFAULT_HARD_LIMIT_SECONDS } from "../config/settings.ts";
|
|
3
|
-
import {
|
|
3
|
+
import type { WakeSourceState } from "../extension/wake-source-state.ts";
|
|
4
|
+
import type {
|
|
5
|
+
EvalDetachedCellManagerOptions,
|
|
6
|
+
EvalDetachedCellSnapshot,
|
|
7
|
+
EvalDetachedCellState,
|
|
8
|
+
EvalDetachedCellStatusEntry,
|
|
9
|
+
} from "./detached-cell-contract.ts";
|
|
4
10
|
import { detachedNotificationSpillPath } from "./detached-cell-notification.ts";
|
|
5
11
|
import { currentDetachedResult, detachedErrorResult, snapshotDetachedCell } from "./detached-cell-snapshot.ts";
|
|
6
12
|
import {
|
|
@@ -8,10 +14,18 @@ import {
|
|
|
8
14
|
allowsDetachedCellTransition,
|
|
9
15
|
detachedCellIsActive,
|
|
10
16
|
} from "./detached-cell-state.ts";
|
|
17
|
+
import { detachedStatusEntries, detachedWakeSourceState } from "./detached-cell-status.ts";
|
|
11
18
|
import { DetachedNotificationQueue } from "./detached-notification-queue.ts";
|
|
12
19
|
import type { EvalKernel, EvalLanguage, EvalToolDetails, EvalToolInput } from "./types.ts";
|
|
13
20
|
|
|
14
|
-
export type
|
|
21
|
+
export type {
|
|
22
|
+
EvalDetachedCellManagerOptions,
|
|
23
|
+
EvalDetachedCellNotification,
|
|
24
|
+
EvalDetachedCellNotifier,
|
|
25
|
+
EvalDetachedCellSnapshot,
|
|
26
|
+
EvalDetachedCellState,
|
|
27
|
+
EvalDetachedCellStatusEntry,
|
|
28
|
+
} from "./detached-cell-contract.ts";
|
|
15
29
|
|
|
16
30
|
type LiveResultProvider = () => AgentToolResult<EvalToolDetails>;
|
|
17
31
|
|
|
@@ -26,6 +40,9 @@ type ManagedCell = {
|
|
|
26
40
|
wasDetached: boolean;
|
|
27
41
|
kernel: EvalKernel | undefined;
|
|
28
42
|
stateRetained: boolean | undefined;
|
|
43
|
+
interruptNote: string | undefined;
|
|
44
|
+
/** Holds the completion notification until the interrupt has reported whether kernel state survived. */
|
|
45
|
+
interruptOutcome: PromiseWithResolvers<void> | undefined;
|
|
29
46
|
liveResult: LiveResultProvider | undefined;
|
|
30
47
|
terminalResult: AgentToolResult<EvalToolDetails> | undefined;
|
|
31
48
|
notificationQueued: boolean;
|
|
@@ -35,44 +52,6 @@ type ManagedCell = {
|
|
|
35
52
|
onHardLimit: ((error: Error) => void) | undefined;
|
|
36
53
|
};
|
|
37
54
|
|
|
38
|
-
export interface EvalDetachedCellSnapshot {
|
|
39
|
-
readonly cellId: string;
|
|
40
|
-
readonly language: EvalLanguage;
|
|
41
|
-
readonly state: EvalDetachedCellState;
|
|
42
|
-
readonly outputTail: string;
|
|
43
|
-
readonly result: AgentToolResult<EvalToolDetails>;
|
|
44
|
-
readonly stateRetained: boolean | undefined;
|
|
45
|
-
/** Set only when the wall-clock kill deadline ended this cell. */
|
|
46
|
-
readonly hardLimitSeconds?: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface EvalDetachedCellNotification {
|
|
50
|
-
readonly cellId: string;
|
|
51
|
-
readonly content: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface EvalDetachedCellNotifier {
|
|
55
|
-
notify(cells: readonly EvalDetachedCellNotification[]): void;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface EvalDetachedCellStatusEntry {
|
|
59
|
-
readonly cellId: string;
|
|
60
|
-
readonly language: EvalLanguage;
|
|
61
|
-
readonly summary?: string;
|
|
62
|
-
readonly startedAtMs: number;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface EvalDetachedCellManagerOptions {
|
|
66
|
-
readonly artifactsDir?: string;
|
|
67
|
-
readonly notifier?: EvalDetachedCellNotifier;
|
|
68
|
-
/** Wall-clock kill deadline in seconds; defaults to the bash-parity 1800s. */
|
|
69
|
-
readonly hardLimitSeconds?: number;
|
|
70
|
-
readonly onStatusChange?: (entries: readonly EvalDetachedCellStatusEntry[]) => void;
|
|
71
|
-
/** Receives a full per-source liveness snapshot on every detached-cell transition; used by the goal builtin. */
|
|
72
|
-
readonly onWakeSourceState?: (state: WakeSourceState) => void;
|
|
73
|
-
readonly now?: () => number;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
55
|
export function hardLimitError(cellId: string, hardLimitSeconds: number): Error {
|
|
77
56
|
const error = new Error(`Eval cell ${cellId} was killed at the ${hardLimitSeconds}s hard limit.`);
|
|
78
57
|
error.name = "TimeoutError";
|
|
@@ -114,6 +93,8 @@ export class EvalDetachedCellManager {
|
|
|
114
93
|
wasDetached: false,
|
|
115
94
|
kernel: undefined,
|
|
116
95
|
stateRetained: undefined,
|
|
96
|
+
interruptNote: undefined,
|
|
97
|
+
interruptOutcome: undefined,
|
|
117
98
|
liveResult: undefined,
|
|
118
99
|
terminalResult: undefined,
|
|
119
100
|
notificationQueued: false,
|
|
@@ -162,13 +143,7 @@ export class EvalDetachedCellManager {
|
|
|
162
143
|
|
|
163
144
|
async stop(cellId: string, reason = "Stopped detached eval cell"): Promise<EvalDetachedCellSnapshot> {
|
|
164
145
|
const cell = this.#get(cellId);
|
|
165
|
-
if (cell.state === "detached")
|
|
166
|
-
const claimed = this.#settle(cell, "cancelled", currentDetachedResult(cell));
|
|
167
|
-
if (claimed && cell.kernel !== undefined) {
|
|
168
|
-
const handle = await cell.kernel.interrupt(reason);
|
|
169
|
-
cell.stateRetained = await handle.stateRetained;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
146
|
+
if (cell.state === "detached") await this.#cancel(cell, reason);
|
|
172
147
|
return this.#snapshot(cell);
|
|
173
148
|
}
|
|
174
149
|
|
|
@@ -221,7 +196,10 @@ export class EvalDetachedCellManager {
|
|
|
221
196
|
if (!cell.notificationQueued) {
|
|
222
197
|
cell.notificationQueued = true;
|
|
223
198
|
this.#notificationQueue.enqueue({
|
|
224
|
-
snapshot: () =>
|
|
199
|
+
snapshot: async () => {
|
|
200
|
+
await cell.interruptOutcome?.promise;
|
|
201
|
+
return this.#snapshot(cell);
|
|
202
|
+
},
|
|
225
203
|
spillPath: cell.spillPath,
|
|
226
204
|
});
|
|
227
205
|
}
|
|
@@ -250,40 +228,34 @@ export class EvalDetachedCellManager {
|
|
|
250
228
|
const foreground = cell.state === "running" && cell.onHardLimit !== undefined;
|
|
251
229
|
cell.hardLimited = true;
|
|
252
230
|
const error = hardLimitError(cell.cellId, cell.hardLimitSeconds);
|
|
253
|
-
if (!this.#settle(cell, "cancelled", currentDetachedResult(cell))) return;
|
|
254
231
|
if (foreground) {
|
|
255
|
-
cell.onHardLimit?.(error);
|
|
232
|
+
if (this.#settle(cell, "cancelled", currentDetachedResult(cell))) cell.onHardLimit?.(error);
|
|
256
233
|
return;
|
|
257
234
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
235
|
+
await this.#cancel(cell, error.message);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async #cancel(cell: ManagedCell, reason: string): Promise<void> {
|
|
239
|
+
const outcome = Promise.withResolvers<void>();
|
|
240
|
+
cell.interruptOutcome = outcome;
|
|
241
|
+
try {
|
|
242
|
+
if (!this.#settle(cell, "cancelled", currentDetachedResult(cell)) || cell.kernel === undefined) return;
|
|
243
|
+
const handle = await cell.kernel.interrupt(reason);
|
|
244
|
+
cell.interruptNote = handle.note;
|
|
245
|
+
cell.stateRetained = await handle.stateRetained;
|
|
246
|
+
} finally {
|
|
247
|
+
outcome.resolve();
|
|
248
|
+
}
|
|
261
249
|
}
|
|
262
250
|
|
|
263
251
|
#emitStatus(): void {
|
|
264
252
|
const liveCells = [...this.#detachedByLanguage.values()];
|
|
265
|
-
this.#onStatusChange?.(
|
|
266
|
-
liveCells.map((cell) => ({
|
|
267
|
-
cellId: cell.cellId,
|
|
268
|
-
language: cell.input.language,
|
|
269
|
-
startedAtMs: cell.startedAtMs,
|
|
270
|
-
...(cell.input.summary === undefined ? {} : { summary: cell.input.summary }),
|
|
271
|
-
})),
|
|
272
|
-
);
|
|
253
|
+
this.#onStatusChange?.(detachedStatusEntries(liveCells));
|
|
273
254
|
this.#emitWakeSourceState(liveCells);
|
|
274
255
|
}
|
|
275
256
|
|
|
276
257
|
#emitWakeSourceState(liveCells: readonly ManagedCell[]): void {
|
|
277
|
-
this.#onWakeSourceState?.(
|
|
278
|
-
source: SENPI_CODEMODE_WAKE_SOURCE,
|
|
279
|
-
activeCount: liveCells.length,
|
|
280
|
-
items: liveCells.map((cell) => ({
|
|
281
|
-
id: cell.cellId,
|
|
282
|
-
description:
|
|
283
|
-
cell.input.summary === undefined || cell.input.summary.length === 0 ? cell.cellId : cell.input.summary,
|
|
284
|
-
startedAtMs: cell.startedAtMs,
|
|
285
|
-
})),
|
|
286
|
-
});
|
|
258
|
+
this.#onWakeSourceState?.(detachedWakeSourceState(liveCells));
|
|
287
259
|
}
|
|
288
260
|
|
|
289
261
|
#snapshot(cell: ManagedCell): EvalDetachedCellSnapshot {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import type { EvalDetachedCellNotification, EvalDetachedCellSnapshot } from "./detached-cell-manager.ts";
|
|
4
|
+
import { interruptionStateNote, unknownInterruptionStateNote } from "./interrupt-note.ts";
|
|
4
5
|
|
|
5
6
|
const NOTIFICATION_TAIL_BYTES = 512;
|
|
6
7
|
|
|
@@ -74,11 +75,9 @@ function outcomeOf(cell: EvalDetachedCellSnapshot): string {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
function stateNoteOf(cell: EvalDetachedCellSnapshot): string {
|
|
77
|
-
if (cell.state
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return "Python kernel was interrupted; its existing variables are preserved.";
|
|
81
|
-
return "Kernel state updated - variables are available to the next eval cell.";
|
|
78
|
+
if (cell.state !== "cancelled") return "Kernel state updated - variables are available to the next eval cell.";
|
|
79
|
+
const note = interruptionStateNote(cell.language, cell.stateRetained) ?? unknownInterruptionStateNote(cell.language);
|
|
80
|
+
return cell.interruptNote === undefined ? note : `${note} ${cell.interruptNote.trim()}`;
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
function safeCellId(cellId: string): string {
|
|
@@ -10,6 +10,7 @@ export interface DetachedCellResultSource {
|
|
|
10
10
|
state: EvalDetachedCellState;
|
|
11
11
|
kernel: EvalKernel | undefined;
|
|
12
12
|
stateRetained: boolean | undefined;
|
|
13
|
+
interruptNote?: string | undefined;
|
|
13
14
|
liveResult: (() => AgentToolResult<EvalToolDetails>) | undefined;
|
|
14
15
|
terminalResult: AgentToolResult<EvalToolDetails> | undefined;
|
|
15
16
|
hardLimited?: boolean;
|
|
@@ -26,6 +27,7 @@ export function snapshotDetachedCell(cell: DetachedCellResultSource, nowMs: numb
|
|
|
26
27
|
outputTail: detachedOutputTail(result),
|
|
27
28
|
result,
|
|
28
29
|
stateRetained: cell.stateRetained,
|
|
30
|
+
...(cell.interruptNote === undefined ? {} : { interruptNote: cell.interruptNote }),
|
|
29
31
|
...(cell.hardLimited === true && cell.hardLimitSeconds !== undefined
|
|
30
32
|
? { hardLimitSeconds: cell.hardLimitSeconds }
|
|
31
33
|
: {}),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SENPI_CODEMODE_WAKE_SOURCE, type WakeSourceState } from "../extension/wake-source-state.ts";
|
|
2
|
+
import type { EvalDetachedCellStatusEntry } from "./detached-cell-manager.ts";
|
|
3
|
+
|
|
4
|
+
export interface LiveDetachedCell {
|
|
5
|
+
readonly cellId: string;
|
|
6
|
+
readonly startedAtMs: number;
|
|
7
|
+
readonly input: { readonly language: EvalDetachedCellStatusEntry["language"]; readonly summary?: string };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function detachedStatusEntries(liveCells: readonly LiveDetachedCell[]): EvalDetachedCellStatusEntry[] {
|
|
11
|
+
return liveCells.map((cell) => ({
|
|
12
|
+
cellId: cell.cellId,
|
|
13
|
+
language: cell.input.language,
|
|
14
|
+
startedAtMs: cell.startedAtMs,
|
|
15
|
+
...(cell.input.summary === undefined ? {} : { summary: cell.input.summary }),
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function detachedWakeSourceState(liveCells: readonly LiveDetachedCell[]): WakeSourceState {
|
|
20
|
+
return {
|
|
21
|
+
source: SENPI_CODEMODE_WAKE_SOURCE,
|
|
22
|
+
activeCount: liveCells.length,
|
|
23
|
+
items: liveCells.map((cell) => ({
|
|
24
|
+
id: cell.cellId,
|
|
25
|
+
description:
|
|
26
|
+
cell.input.summary === undefined || cell.input.summary.length === 0 ? cell.cellId : cell.input.summary,
|
|
27
|
+
startedAtMs: cell.startedAtMs,
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -40,6 +40,7 @@ export function createDetachedControlResult(snapshot: EvalDetachedCellSnapshot):
|
|
|
40
40
|
`Eval cell ${snapshot.cellId} (${snapshot.language}) is ${snapshot.state}.`,
|
|
41
41
|
output.length === 0 ? "(no buffered output)" : output,
|
|
42
42
|
...(terminationNote === undefined ? [] : [terminationNote]),
|
|
43
|
+
...(snapshot.interruptNote === undefined ? [] : [snapshot.interruptNote.trim()]),
|
|
43
44
|
].join("\n");
|
|
44
45
|
return {
|
|
45
46
|
content: [{ type: "text", text }, ...snapshot.result.content.filter((part) => part.type === "image")],
|
|
@@ -2,7 +2,7 @@ import type { EvalDetachedCellNotifier, EvalDetachedCellSnapshot } from "./detac
|
|
|
2
2
|
import { buildDetachedCellNotification } from "./detached-cell-notification.ts";
|
|
3
3
|
|
|
4
4
|
export interface PendingDetachedNotification {
|
|
5
|
-
readonly snapshot: () => EvalDetachedCellSnapshot
|
|
5
|
+
readonly snapshot: () => EvalDetachedCellSnapshot | Promise<EvalDetachedCellSnapshot>;
|
|
6
6
|
readonly spillPath: string | undefined;
|
|
7
7
|
}
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ export class DetachedNotificationQueue {
|
|
|
30
30
|
const flush = Promise.resolve().then(async () => {
|
|
31
31
|
const pending = this.#pending.splice(0);
|
|
32
32
|
const notifications = await Promise.all(
|
|
33
|
-
pending.map(async (item) => await buildDetachedCellNotification(item.snapshot(), item.spillPath)),
|
|
33
|
+
pending.map(async (item) => await buildDetachedCellNotification(await item.snapshot(), item.spillPath)),
|
|
34
34
|
);
|
|
35
35
|
this.#notifier?.notify(notifications);
|
|
36
36
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EvalLanguage } from "./types.ts";
|
|
1
|
+
import type { EvalLanguage, KernelInterruptHandle } from "./types.ts";
|
|
2
2
|
|
|
3
3
|
const TIMEOUT_STATE_GRACE_MS = 5_500;
|
|
4
4
|
|
|
@@ -13,30 +13,40 @@ function fallbackTimeoutMessage(base: string): string {
|
|
|
13
13
|
*/
|
|
14
14
|
export async function describeTimeoutState(
|
|
15
15
|
error: Error,
|
|
16
|
-
execution: { readonly
|
|
16
|
+
execution: { readonly interruptHandle: Promise<KernelInterruptHandle> | undefined },
|
|
17
17
|
): Promise<Error> {
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
18
|
+
const pending = execution.interruptHandle;
|
|
19
|
+
if (pending === undefined) {
|
|
20
20
|
error.message = fallbackTimeoutMessage(error.message);
|
|
21
21
|
return error;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
]).finally(() => {
|
|
30
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
31
|
-
});
|
|
32
|
-
if (retained === undefined) error.message = fallbackTimeoutMessage(error.message);
|
|
33
|
-
else if (retained)
|
|
23
|
+
const outcome = await withinGrace(
|
|
24
|
+
pending.then(async (handle) => ({ retained: await handle.stateRetained, note: handle.note })),
|
|
25
|
+
TIMEOUT_STATE_GRACE_MS,
|
|
26
|
+
);
|
|
27
|
+
if (outcome === undefined) error.message = fallbackTimeoutMessage(error.message);
|
|
28
|
+
else if (outcome.retained)
|
|
34
29
|
error.message = `${error.message} The kernel remains running; its existing variables are preserved.`;
|
|
35
30
|
else
|
|
36
31
|
error.message = `${error.message} The kernel was unresponsive and restarted; variables from earlier cells are lost.`;
|
|
32
|
+
if (outcome?.note !== undefined) error.message = `${error.message} ${outcome.note.trim()}`;
|
|
37
33
|
return error;
|
|
38
34
|
}
|
|
39
35
|
|
|
36
|
+
async function withinGrace<T>(operation: Promise<T>, graceMs: number): Promise<T | undefined> {
|
|
37
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
38
|
+
try {
|
|
39
|
+
return await Promise.race([
|
|
40
|
+
operation,
|
|
41
|
+
new Promise<undefined>((resolve) => {
|
|
42
|
+
timer = setTimeout(() => resolve(undefined), graceMs);
|
|
43
|
+
}),
|
|
44
|
+
]);
|
|
45
|
+
} finally {
|
|
46
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
const LANGUAGE_LABEL: Record<EvalLanguage, string> = {
|
|
41
51
|
py: "Python kernel",
|
|
42
52
|
js: "JavaScript worker",
|
|
@@ -56,3 +66,7 @@ export function interruptionStateNote(language: EvalLanguage, stateRetained: boo
|
|
|
56
66
|
if (stateRetained) return `${label} was interrupted and remains running; its existing variables are preserved.`;
|
|
57
67
|
return `${label} was unresponsive to interrupt and was restarted; variables from earlier cells are lost.`;
|
|
58
68
|
}
|
|
69
|
+
|
|
70
|
+
export function unknownInterruptionStateNote(language: EvalLanguage): string {
|
|
71
|
+
return `${LANGUAGE_LABEL[language]} interrupt outcome is unknown; re-establish any variables the next cell needs.`;
|
|
72
|
+
}
|
package/src/tool/types.ts
CHANGED
|
@@ -112,6 +112,8 @@ export interface EvalKernelRunInput {
|
|
|
112
112
|
export interface KernelInterruptHandle {
|
|
113
113
|
/** Resolves once the kernel knows whether user state survived the interrupt. */
|
|
114
114
|
readonly stateRetained: Promise<boolean>;
|
|
115
|
+
/** Extra outcome detail worth showing the model, e.g. that a blocked worker was abandoned. */
|
|
116
|
+
readonly note?: string;
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
export interface EvalKernel {
|