@code-yeongyu/senpi-codemode 2026.10.6 → 2026.10.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 +19 -0
- package/README.md +4 -0
- package/package.json +4 -4
- package/src/bridge/reserved.ts +10 -0
- package/src/bridges/eval-virtual-schemas.ts +79 -0
- package/src/bridges/handle-bridge.ts +72 -0
- package/src/bridges/reserved-dispatch.ts +20 -2
- package/src/bridges/schema-bridge.ts +4 -0
- package/src/completion/handle.ts +69 -0
- package/src/completion/tool-bridge.ts +31 -12
- package/src/extension/bridge-dispatch.ts +41 -0
- package/src/extension/bridge-tool-call.ts +5 -1
- package/src/extension/host-facts.ts +18 -0
- package/src/extension/removed-tool-hints.ts +15 -0
- package/src/extension/runtime-factory.ts +3 -0
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager.ts +17 -4
- package/src/extension/start-recovery.ts +111 -0
- package/src/handles/completion-handles.ts +202 -0
- package/src/handles/handle-args.ts +103 -0
- package/src/handles/handle-registry.ts +164 -0
- package/src/handles/wait.ts +214 -0
- package/src/handles/watch-queue.ts +102 -0
- package/src/index.ts +99 -94
- package/src/kernels/jl/handles.jl +124 -0
- package/src/kernels/jl/prelude.jl +9 -0
- package/src/kernels/jl/runner.jl +6 -1
- package/src/kernels/js/handles.js +93 -0
- package/src/kernels/js/kernel-tools-define.d.ts +6 -0
- package/src/kernels/js/kernel-tools-define.js +3 -1
- package/src/kernels/js/kernel-tools-registry.d.ts +2 -0
- package/src/kernels/js/kernel-tools-registry.js +18 -9
- package/src/kernels/js/kernel-tools-schema.js +6 -2
- package/src/kernels/js/worker-runtime.js +10 -1
- package/src/kernels/py/host-cell.ts +42 -0
- package/src/kernels/py/kernel-contract.ts +2 -0
- package/src/kernels/py/kernel.ts +48 -4
- package/src/kernels/py/prelude.py +140 -4
- package/src/kernels/rb/handles.rb +97 -0
- package/src/kernels/rb/paths.rb +37 -0
- package/src/kernels/rb/prelude.rb +15 -38
- package/src/kernels/rb/runner.rb +7 -7
- package/src/tool/cell-handler.ts +21 -18
- package/src/tool/cell-reserved-dispatch.ts +51 -0
- package/src/tool/eval-tool-options.ts +3 -0
- package/src/tool/run-eval-cell.ts +2 -0
- package/src/tool/types.ts +15 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EvalHandleError,
|
|
3
|
+
type HandleOutcome,
|
|
4
|
+
type HandlePhase,
|
|
5
|
+
type HandleRef,
|
|
6
|
+
type HandleSnapshot,
|
|
7
|
+
type HandleWatch,
|
|
8
|
+
} from "@code-yeongyu/senpi";
|
|
9
|
+
import { refKey, type WaitRequest } from "./handle-args.ts";
|
|
10
|
+
|
|
11
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
12
|
+
|
|
13
|
+
export interface WaitBackend {
|
|
14
|
+
watch(refs: readonly HandleRef[]): Promise<HandleWatch>;
|
|
15
|
+
result(ref: HandleRef): Promise<HandleOutcome>;
|
|
16
|
+
readonly signal?: AbortSignal;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type WaitAnyResult = { readonly index: number; readonly ref: HandleRef; readonly value: unknown };
|
|
20
|
+
|
|
21
|
+
/** A handle's own failure, raised by `wait(..., {mode: "all"})` and `control.wait()`; `code` is in the text. */
|
|
22
|
+
export class HandleOutcomeError extends Error {
|
|
23
|
+
readonly name = "HandleOutcomeError";
|
|
24
|
+
readonly code: string;
|
|
25
|
+
readonly ref: HandleRef;
|
|
26
|
+
readonly details: unknown;
|
|
27
|
+
|
|
28
|
+
constructor(outcome: Extract<HandleOutcome, { status: "rejected" }>) {
|
|
29
|
+
super(`${outcome.error.code}: ${outcome.error.message}`);
|
|
30
|
+
this.code = outcome.error.code;
|
|
31
|
+
this.ref = outcome.ref;
|
|
32
|
+
this.details = outcome.error.details;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class WaitAllRejectedError extends Error {
|
|
37
|
+
readonly name = "WaitAllRejectedError";
|
|
38
|
+
readonly code = "eval_wait_all_rejected";
|
|
39
|
+
readonly outcomes: readonly HandleOutcome[];
|
|
40
|
+
|
|
41
|
+
constructor(outcomes: readonly HandleOutcome[]) {
|
|
42
|
+
const summary = outcomes
|
|
43
|
+
.map((outcome) =>
|
|
44
|
+
outcome.status === "rejected" ? `${outcome.ref.id}: ${outcome.error.code}` : outcome.ref.id,
|
|
45
|
+
)
|
|
46
|
+
.join("; ");
|
|
47
|
+
super(`eval_wait_all_rejected: wait(any) found no successful handle among ${outcomes.length}; ${summary}`);
|
|
48
|
+
this.outcomes = outcomes;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const TERMINAL: ReadonlySet<HandlePhase> = new Set(["succeeded", "failed", "cancelled", "lost"]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The in-cell barrier. Subscribes once per distinct ref, keeps duplicate slots, settles according to the
|
|
56
|
+
* mode, and never cancels work: a timeout only closes the subscription. The timeout runs from entry and,
|
|
57
|
+
* like cancelling the cell, also ends a wait that is still subscribing or fetching a result. Parked time
|
|
58
|
+
* is the caller's concern (the bridge-call path pauses the run budget around this call).
|
|
59
|
+
*/
|
|
60
|
+
export async function waitForHandles(request: WaitRequest, backend: WaitBackend): Promise<unknown> {
|
|
61
|
+
const { refs, mode } = request;
|
|
62
|
+
if (refs.length === 0) {
|
|
63
|
+
if (mode === "any") throw new EvalHandleError("eval_wait_empty", "wait(any) needs at least one handle");
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const unique = [...new Map(refs.map((ref) => [refKey(ref), ref])).values()];
|
|
67
|
+
const outcomes = new Map<string, HandleOutcome>();
|
|
68
|
+
const revisions = new Map<string, number>();
|
|
69
|
+
const stop = interruption(request.timeoutSeconds, backend.signal, refs, outcomes);
|
|
70
|
+
let watch: HandleWatch | undefined;
|
|
71
|
+
const finish = (): unknown => {
|
|
72
|
+
switch (mode) {
|
|
73
|
+
case "all":
|
|
74
|
+
return refs.map((ref) => settledValue(outcomes, ref));
|
|
75
|
+
case "settled":
|
|
76
|
+
return refs.map((ref) => outcomeOf(outcomes, ref));
|
|
77
|
+
case "any":
|
|
78
|
+
return firstSuccess(refs, outcomes) ?? rejectAll(refs, outcomes);
|
|
79
|
+
default:
|
|
80
|
+
return assertNever(mode);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
// Returns true once the mode can settle without waiting for the remaining handles.
|
|
84
|
+
const absorb = async (snapshot: HandleSnapshot): Promise<boolean> => {
|
|
85
|
+
const key = refKey(snapshot.ref);
|
|
86
|
+
const known = revisions.get(key);
|
|
87
|
+
if (known !== undefined && snapshot.revision <= known) return false;
|
|
88
|
+
revisions.set(key, snapshot.revision);
|
|
89
|
+
if (snapshot.ref.kind === "workpool" && snapshot.phase === "pending" && snapshot.host_status === "open") {
|
|
90
|
+
throw new EvalHandleError(
|
|
91
|
+
"eval_pool_open",
|
|
92
|
+
`workpool ${snapshot.ref.id} is still open; close() it before waiting`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (!TERMINAL.has(snapshot.phase) || outcomes.has(key)) return false;
|
|
96
|
+
const outcome = await stop.race(backend.result(snapshot.ref));
|
|
97
|
+
outcomes.set(key, outcome);
|
|
98
|
+
if (mode === "all" && outcome.status === "rejected") throw new HandleOutcomeError(outcome);
|
|
99
|
+
if (mode === "any" && outcome.status === "fulfilled") return true;
|
|
100
|
+
return outcomes.size === unique.length;
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
const subscribing = backend.watch(unique);
|
|
104
|
+
// A watch that resolves after the wait already ended is closed, so its subscription never leaks.
|
|
105
|
+
void subscribing.then(
|
|
106
|
+
(late) => {
|
|
107
|
+
if (stop.ended) late.close();
|
|
108
|
+
},
|
|
109
|
+
() => undefined,
|
|
110
|
+
);
|
|
111
|
+
watch = await stop.race(subscribing);
|
|
112
|
+
for (const snapshot of watch.initial) {
|
|
113
|
+
if (await absorb(snapshot)) return finish();
|
|
114
|
+
}
|
|
115
|
+
if (outcomes.size === unique.length) return finish();
|
|
116
|
+
if (request.timeoutSeconds === 0) throw timeoutError(0, refs, outcomes);
|
|
117
|
+
const iterator = watch.updates[Symbol.asyncIterator]();
|
|
118
|
+
try {
|
|
119
|
+
while (true) {
|
|
120
|
+
const step = await stop.race(iterator.next());
|
|
121
|
+
if (step.done) return finish();
|
|
122
|
+
if (await absorb(step.value)) return finish();
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
await iterator.return?.();
|
|
126
|
+
}
|
|
127
|
+
} finally {
|
|
128
|
+
stop.end();
|
|
129
|
+
watch?.close();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One deadline armed at entry plus the cell's abort; `race` rejects any pending step once either fires. */
|
|
134
|
+
function interruption(
|
|
135
|
+
timeoutSeconds: number | undefined,
|
|
136
|
+
signal: AbortSignal | undefined,
|
|
137
|
+
refs: readonly HandleRef[],
|
|
138
|
+
outcomes: ReadonlyMap<string, HandleOutcome>,
|
|
139
|
+
) {
|
|
140
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
141
|
+
let onAbort: (() => void) | undefined;
|
|
142
|
+
let ended = false;
|
|
143
|
+
const fired = new Promise<never>((_resolve, reject) => {
|
|
144
|
+
if (timeoutSeconds !== undefined && timeoutSeconds > 0) {
|
|
145
|
+
// A timer longer than the platform maximum fires at once, so a long deadline is armed in chunks.
|
|
146
|
+
const deadline = Date.now() + timeoutSeconds * 1_000;
|
|
147
|
+
const arm = (): void => {
|
|
148
|
+
const remainingMs = deadline - Date.now();
|
|
149
|
+
if (remainingMs <= 0) {
|
|
150
|
+
reject(timeoutError(timeoutSeconds, refs, outcomes));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
timer = setTimeout(arm, Math.min(remainingMs, MAX_TIMER_MS));
|
|
154
|
+
};
|
|
155
|
+
arm();
|
|
156
|
+
}
|
|
157
|
+
if (signal !== undefined) {
|
|
158
|
+
onAbort = () =>
|
|
159
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("wait() cancelled with its cell"));
|
|
160
|
+
if (signal.aborted) onAbort();
|
|
161
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
fired.catch(() => undefined);
|
|
165
|
+
return {
|
|
166
|
+
get ended(): boolean {
|
|
167
|
+
return ended;
|
|
168
|
+
},
|
|
169
|
+
race<T>(step: Promise<T>): Promise<T> {
|
|
170
|
+
return Promise.race([step, fired]);
|
|
171
|
+
},
|
|
172
|
+
end(): void {
|
|
173
|
+
ended = true;
|
|
174
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
175
|
+
if (signal !== undefined && onAbort !== undefined) signal.removeEventListener("abort", onAbort);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function timeoutError(seconds: number, refs: readonly HandleRef[], outcomes: ReadonlyMap<string, HandleOutcome>) {
|
|
181
|
+
const settled = refs.filter((ref) => outcomes.has(refKey(ref))).length;
|
|
182
|
+
return new EvalHandleError(
|
|
183
|
+
"eval_wait_timeout",
|
|
184
|
+
`wait() timed out after ${seconds}s; ${settled}/${refs.length} handles settled; work was not cancelled`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function outcomeOf(outcomes: ReadonlyMap<string, HandleOutcome>, ref: HandleRef): HandleOutcome {
|
|
189
|
+
const outcome = outcomes.get(refKey(ref));
|
|
190
|
+
if (outcome === undefined) throw new EvalHandleError("eval_handle_pending", `${ref.id} did not settle`);
|
|
191
|
+
return outcome;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function settledValue(outcomes: ReadonlyMap<string, HandleOutcome>, ref: HandleRef): unknown {
|
|
195
|
+
const outcome = outcomeOf(outcomes, ref);
|
|
196
|
+
if (outcome.status === "rejected") throw new HandleOutcomeError(outcome);
|
|
197
|
+
return outcome.value;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function firstSuccess(refs: readonly HandleRef[], outcomes: ReadonlyMap<string, HandleOutcome>) {
|
|
201
|
+
for (const [index, ref] of refs.entries()) {
|
|
202
|
+
const outcome = outcomes.get(refKey(ref));
|
|
203
|
+
if (outcome?.status === "fulfilled") return { index, ref, value: outcome.value } satisfies WaitAnyResult;
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function rejectAll(refs: readonly HandleRef[], outcomes: ReadonlyMap<string, HandleOutcome>): never {
|
|
209
|
+
throw new WaitAllRejectedError(refs.map((ref) => outcomeOf(outcomes, ref)));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function assertNever(value: never): never {
|
|
213
|
+
throw new TypeError(`Unsupported wait mode: ${String(value)}`);
|
|
214
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { HandleSnapshot, HandleWatch } from "@code-yeongyu/senpi";
|
|
2
|
+
import { refKey } from "./handle-args.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A pull-side buffer for handle snapshots. Pushes queue up until the consumer iterates, so a change
|
|
6
|
+
* that lands between subscription and `initial` is delivered exactly once; `close()` is idempotent
|
|
7
|
+
* and ends the iterator.
|
|
8
|
+
*/
|
|
9
|
+
export class WatchQueue {
|
|
10
|
+
readonly #keys: ReadonlySet<string> | undefined;
|
|
11
|
+
readonly #queue: HandleSnapshot[] = [];
|
|
12
|
+
#wake: (() => void) | undefined;
|
|
13
|
+
#closed = false;
|
|
14
|
+
#failure: unknown;
|
|
15
|
+
#failed = false;
|
|
16
|
+
|
|
17
|
+
/** Without keys every snapshot is accepted (used by the merger); with keys only matching refs are. */
|
|
18
|
+
constructor(keys?: readonly string[]) {
|
|
19
|
+
this.#keys = keys === undefined ? undefined : new Set(keys);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get closed(): boolean {
|
|
23
|
+
return this.#closed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
push(snapshot: HandleSnapshot): void {
|
|
27
|
+
if (this.#closed || (this.#keys !== undefined && !this.#keys.has(refKey(snapshot.ref)))) return;
|
|
28
|
+
this.#queue.push(snapshot);
|
|
29
|
+
this.#wake?.();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
close(): void {
|
|
33
|
+
if (this.#closed) return;
|
|
34
|
+
this.#closed = true;
|
|
35
|
+
this.#wake?.();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Ends the stream with an error: the next pull rejects instead of reporting a clean end. */
|
|
39
|
+
fail(error: unknown): void {
|
|
40
|
+
if (this.#closed) return;
|
|
41
|
+
this.#failed = true;
|
|
42
|
+
this.#failure = error;
|
|
43
|
+
this.close();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
toWatch(initial: readonly HandleSnapshot[], onClose?: () => void): HandleWatch {
|
|
47
|
+
const close = (): void => {
|
|
48
|
+
this.close();
|
|
49
|
+
onClose?.();
|
|
50
|
+
};
|
|
51
|
+
const next = async (): Promise<IteratorResult<HandleSnapshot>> => {
|
|
52
|
+
while (this.#queue.length === 0 && !this.#closed) {
|
|
53
|
+
await new Promise<void>((resolve) => {
|
|
54
|
+
this.#wake = resolve;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
this.#wake = undefined;
|
|
58
|
+
const value = this.#queue.shift();
|
|
59
|
+
if (value !== undefined) return { done: false, value };
|
|
60
|
+
if (this.#failed) throw this.#failure;
|
|
61
|
+
return { done: true, value: undefined };
|
|
62
|
+
};
|
|
63
|
+
const updates: AsyncIterable<HandleSnapshot> = {
|
|
64
|
+
[Symbol.asyncIterator]: () => ({
|
|
65
|
+
next,
|
|
66
|
+
return: async (): Promise<IteratorResult<HandleSnapshot>> => {
|
|
67
|
+
close();
|
|
68
|
+
return { done: true, value: undefined };
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
return { initial, updates, close };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One watch over several: `initial` keeps the caller's order, updates from every part are drained eagerly. */
|
|
77
|
+
export function mergeWatches(ordered: readonly HandleSnapshot[], parts: readonly HandleWatch[]): HandleWatch {
|
|
78
|
+
if (parts.length === 1 && parts[0] !== undefined) return { ...parts[0], initial: ordered };
|
|
79
|
+
const queue = new WatchQueue();
|
|
80
|
+
let open = parts.length;
|
|
81
|
+
const drain = async (part: HandleWatch): Promise<void> => {
|
|
82
|
+
try {
|
|
83
|
+
for await (const snapshot of part.updates) queue.push(snapshot);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
queue.fail(error);
|
|
86
|
+
} finally {
|
|
87
|
+
open -= 1;
|
|
88
|
+
if (open === 0) queue.close();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
if (parts.length === 0) queue.close();
|
|
92
|
+
for (const part of parts) void drain(part);
|
|
93
|
+
const merged = queue.toWatch(ordered);
|
|
94
|
+
return {
|
|
95
|
+
initial: merged.initial,
|
|
96
|
+
updates: merged.updates,
|
|
97
|
+
close: () => {
|
|
98
|
+
for (const part of parts) part.close();
|
|
99
|
+
merged.close();
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as os from "node:os";
|
|
2
1
|
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
3
2
|
import type { AgentExecuteTool } from "./bridges/agent-bridge.ts";
|
|
4
3
|
import type { EvalSchemaToolInfo } from "./bridges/schema-bridge.ts";
|
|
@@ -14,18 +13,22 @@ import {
|
|
|
14
13
|
import { EvalNotifier } from "./extension/eval-notifier.ts";
|
|
15
14
|
import { EVAL_CELLS_STATUS_KEY } from "./extension/eval-status.ts";
|
|
16
15
|
import { EvalStatusTicker } from "./extension/eval-status-ticker.ts";
|
|
16
|
+
import { hostLine, modelIdFrom } from "./extension/host-facts.ts";
|
|
17
17
|
import { activeKernelPreludes, kernelPreludeDocsKey, promptKernelPreludes } from "./extension/kernel-preludes.ts";
|
|
18
|
+
import { registerRemovedToolHints } from "./extension/removed-tool-hints.ts";
|
|
18
19
|
import {
|
|
19
20
|
createExecuteTool,
|
|
20
21
|
createRuntime,
|
|
21
22
|
enabledLanguagesFrom,
|
|
22
23
|
type SessionRuntime,
|
|
23
24
|
} from "./extension/runtime-factory.ts";
|
|
24
|
-
import { jsRuntimeInfo
|
|
25
|
+
import { jsRuntimeInfo } from "./extension/runtime-info.ts";
|
|
25
26
|
import type { CodemodeSessionManager, CreateCodemodeSessionManagerOptions } from "./extension/session-manager.ts";
|
|
26
27
|
import { SessionManagerProxy } from "./extension/session-manager-proxy.ts";
|
|
27
28
|
import { activeBunSkillPath, registerBunSkillContribution } from "./extension/skill-contribution.ts";
|
|
29
|
+
import { StartRecovery, withStartRecovery } from "./extension/start-recovery.ts";
|
|
28
30
|
import { WAKE_SOURCE_STATE_EVENT, type WakeSourceState } from "./extension/wake-source-state.ts";
|
|
31
|
+
import { HandleRegistry } from "./handles/handle-registry.ts";
|
|
29
32
|
import type { KernelToolsCapability } from "./kernels/js/kernel-tools-types.ts";
|
|
30
33
|
import { EvalDetachedCellManager, type EvalDetachedCellStatusEntry } from "./tool/detached-cell-manager.ts";
|
|
31
34
|
import {
|
|
@@ -36,12 +39,9 @@ import {
|
|
|
36
39
|
import { createEvalTool } from "./tool/eval-tool.ts";
|
|
37
40
|
import { renderEvalCall, renderEvalResult } from "./tool/render.ts";
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"session_before_switch",
|
|
43
|
-
"session_before_fork",
|
|
44
|
-
] as const;
|
|
42
|
+
// session_before_switch / session_before_fork are veto points another extension can cancel, so they never
|
|
43
|
+
// tear the runtime down; a switch or fork that goes ahead emits session_shutdown before the old session ends.
|
|
44
|
+
const SESSION_LIFECYCLE_EVENTS = ["session_start", "session_shutdown"] as const;
|
|
45
45
|
|
|
46
46
|
type SessionLifecycleEvent = (typeof SESSION_LIFECYCLE_EVENTS)[number];
|
|
47
47
|
|
|
@@ -93,6 +93,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
93
93
|
let activeModelId: string | undefined;
|
|
94
94
|
let activeContext: ExtensionContext | undefined;
|
|
95
95
|
let activeCells: EvalDetachedCellManager | undefined;
|
|
96
|
+
let activeHandles: HandleRegistry | undefined;
|
|
96
97
|
let promptPreludeDocs = "";
|
|
97
98
|
const notifier = new EvalNotifier({
|
|
98
99
|
sendMessage: (message, notifyOptions) => pi.sendMessage(message, notifyOptions),
|
|
@@ -122,10 +123,12 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
122
123
|
pi.rpc?.emit(WAKE_SOURCE_STATE_EVENT, state);
|
|
123
124
|
pi.events?.emit(WAKE_SOURCE_STATE_EVENT, state);
|
|
124
125
|
};
|
|
126
|
+
const recovery = new StartRecovery(async (event, ctx) => await startSession(event, ctx));
|
|
125
127
|
const registerEvalForRuntime = (
|
|
126
128
|
runtime: SessionRuntime,
|
|
127
129
|
modelId: string | undefined,
|
|
128
130
|
cellManager: EvalDetachedCellManager,
|
|
131
|
+
handles: HandleRegistry,
|
|
129
132
|
): void => {
|
|
130
133
|
const onCellSettled = (payload: EvalExecutionEventPayload): void => {
|
|
131
134
|
if (activeCells !== cellManager) return;
|
|
@@ -140,93 +143,104 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
140
143
|
const preludes = promptKernelPreludes(pi);
|
|
141
144
|
promptPreludeDocs = kernelPreludeDocsKey(preludes);
|
|
142
145
|
pi.registerTool(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
146
|
+
withStartRecovery(
|
|
147
|
+
recovery,
|
|
148
|
+
createEvalTool({
|
|
149
|
+
enabledLanguages: runtime.enabledLanguages,
|
|
150
|
+
kernelManager: manager,
|
|
151
|
+
cellTimeoutSeconds: runtime.settings.cellTimeoutSeconds,
|
|
152
|
+
foregroundWindowSeconds: resolveForegroundWindowSeconds(runtime.settings),
|
|
153
|
+
runBudgetSeconds: resolveRunBudgetSeconds(runtime.settings),
|
|
154
|
+
hardLimitSeconds: resolveHardLimitSeconds(runtime.settings),
|
|
155
|
+
executeTool: runtime.executeTool,
|
|
156
|
+
listTools: () => pi.getAllTools(),
|
|
157
|
+
complete,
|
|
158
|
+
settings: runtime.settings,
|
|
159
|
+
artifactsDir: runtime.artifactsDir,
|
|
160
|
+
cellManager,
|
|
161
|
+
handles,
|
|
162
|
+
executionTracker: manager,
|
|
163
|
+
onCellSettled,
|
|
164
|
+
renderers,
|
|
165
|
+
monitor,
|
|
166
|
+
spawns: runtime.spawns,
|
|
167
|
+
spawnDefaultAgent: runtime.settings.taskTools.task,
|
|
168
|
+
hostLine: hostLine(),
|
|
169
|
+
runtimes: runtime.runtimes,
|
|
170
|
+
kernelPreludes: () => activeKernelPreludes(pi),
|
|
171
|
+
promptKernelPreludes: preludes,
|
|
172
|
+
...(bunSkillPath === undefined ? {} : { bunSkillPath }),
|
|
173
|
+
...(modelId === undefined ? {} : { modelId }),
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
169
176
|
);
|
|
170
177
|
};
|
|
171
178
|
const dropRuntime = async (): Promise<void> => {
|
|
172
179
|
const cells = activeCells;
|
|
180
|
+
const handles = activeHandles;
|
|
173
181
|
activeRuntime = undefined;
|
|
174
182
|
activeModelId = undefined;
|
|
175
183
|
activeCells = undefined;
|
|
184
|
+
activeHandles = undefined;
|
|
176
185
|
statusTicker.stop();
|
|
186
|
+
// Fail every saved handle closed before the kernels go: a late wait() must never reach a successor generation.
|
|
187
|
+
handles?.dispose();
|
|
177
188
|
await cells?.dispose();
|
|
178
189
|
activeContext = undefined;
|
|
179
190
|
await manager.dispose();
|
|
180
191
|
};
|
|
181
192
|
pi.registerTool(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
executeTool: createExecuteTool(pi),
|
|
190
|
-
listTools: () => pi.getAllTools(),
|
|
191
|
-
complete,
|
|
192
|
-
settings: defaultCodemodeSettings,
|
|
193
|
-
cellManager: new EvalDetachedCellManager({
|
|
194
|
-
notifier,
|
|
195
|
-
maxDetachedCells: resolveMaxDetachedCells(defaultCodemodeSettings),
|
|
196
|
-
retainedResultsBytes: resolveRetainedResultsBytes(defaultCodemodeSettings),
|
|
197
|
-
hardLimitSeconds: resolveHardLimitSeconds(defaultCodemodeSettings),
|
|
193
|
+
withStartRecovery(
|
|
194
|
+
recovery,
|
|
195
|
+
createEvalTool({
|
|
196
|
+
enabledLanguages: { py: true, js: true, rb: true, jl: true },
|
|
197
|
+
kernelManager: manager,
|
|
198
|
+
cellTimeoutSeconds: defaultCodemodeSettings.cellTimeoutSeconds,
|
|
199
|
+
foregroundWindowSeconds: resolveForegroundWindowSeconds(defaultCodemodeSettings),
|
|
198
200
|
runBudgetSeconds: resolveRunBudgetSeconds(defaultCodemodeSettings),
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
hardLimitSeconds: resolveHardLimitSeconds(defaultCodemodeSettings),
|
|
202
|
+
executeTool: createExecuteTool(pi),
|
|
203
|
+
listTools: () => pi.getAllTools(),
|
|
204
|
+
complete,
|
|
205
|
+
settings: defaultCodemodeSettings,
|
|
206
|
+
cellManager: new EvalDetachedCellManager({
|
|
207
|
+
notifier,
|
|
208
|
+
maxDetachedCells: resolveMaxDetachedCells(defaultCodemodeSettings),
|
|
209
|
+
retainedResultsBytes: resolveRetainedResultsBytes(defaultCodemodeSettings),
|
|
210
|
+
hardLimitSeconds: resolveHardLimitSeconds(defaultCodemodeSettings),
|
|
211
|
+
runBudgetSeconds: resolveRunBudgetSeconds(defaultCodemodeSettings),
|
|
212
|
+
onStatusChange: showDetachedCells,
|
|
213
|
+
onWakeSourceState: emitWakeSourceState,
|
|
214
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
215
|
+
}),
|
|
216
|
+
executionTracker: manager,
|
|
217
|
+
renderers,
|
|
218
|
+
// The baseline tool is registered before extensions such as monitor load.
|
|
219
|
+
monitor: false,
|
|
220
|
+
hostLine: hostLine(),
|
|
221
|
+
runtimes: { js: jsRuntimeInfo() },
|
|
222
|
+
...(bunSkillPath === undefined ? {} : { bunSkillPath }),
|
|
202
223
|
}),
|
|
203
|
-
|
|
204
|
-
renderers,
|
|
205
|
-
// The baseline tool is registered before extensions such as monitor load.
|
|
206
|
-
monitor: false,
|
|
207
|
-
hostLine: hostLine(),
|
|
208
|
-
runtimes: { js: jsRuntimeInfo() },
|
|
209
|
-
...(bunSkillPath === undefined ? {} : { bunSkillPath }),
|
|
210
|
-
}),
|
|
211
|
-
);
|
|
212
|
-
pi.registerRemovedToolHint(
|
|
213
|
-
"exec",
|
|
214
|
-
'exec was removed; use eval({ language: "js", code }) instead. Long eval cells detach on their own and notify when complete.',
|
|
215
|
-
);
|
|
216
|
-
pi.registerRemovedToolHint(
|
|
217
|
-
"wait",
|
|
218
|
-
'wait was removed; detached eval cells notify when complete. Use eval({ action: "peek"|"stop", cell_id }) to inspect or stop one.',
|
|
224
|
+
),
|
|
219
225
|
);
|
|
226
|
+
registerRemovedToolHints(pi);
|
|
220
227
|
registerBunSkillContribution(pi);
|
|
221
228
|
|
|
222
|
-
|
|
229
|
+
const startSession = async (event: unknown, ctx: ExtensionContext): Promise<void> => {
|
|
223
230
|
const previousCells = activeCells;
|
|
231
|
+
const previousHandles = activeHandles;
|
|
224
232
|
activeCells = undefined;
|
|
233
|
+
activeHandles = undefined;
|
|
234
|
+
previousHandles?.dispose();
|
|
225
235
|
await previousCells?.dispose();
|
|
226
236
|
const generation = manager.beginReplacement();
|
|
227
|
-
const
|
|
237
|
+
const handles = new HandleRegistry({ ownerSessionId: ctx.sessionManager.getSessionId() });
|
|
238
|
+
const runtime = await createRuntime(pi, ctx, event, complete, options, handles);
|
|
228
239
|
const replaced = await manager.replace(generation, runtime.manager);
|
|
229
|
-
if (!replaced)
|
|
240
|
+
if (!replaced) {
|
|
241
|
+
handles.dispose();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
230
244
|
notifier.reset();
|
|
231
245
|
activeContext = ctx;
|
|
232
246
|
const cellManager = new EvalDetachedCellManager({
|
|
@@ -242,15 +256,18 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
242
256
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
243
257
|
});
|
|
244
258
|
activeCells = cellManager;
|
|
259
|
+
activeHandles = handles;
|
|
245
260
|
// The goal builtin clears its per-session counts at session_start; re-publish our snapshot.
|
|
246
261
|
cellManager.publishWakeSourceState();
|
|
247
262
|
activeRuntime = runtime;
|
|
248
263
|
activeModelId = ctx.model?.id;
|
|
249
|
-
registerEvalForRuntime(runtime, activeModelId, cellManager);
|
|
264
|
+
registerEvalForRuntime(runtime, activeModelId, cellManager, handles);
|
|
265
|
+
};
|
|
266
|
+
pi.on("session_start", async (event, ctx) => await recovery.runStart(event, ctx));
|
|
267
|
+
pi.on("session_shutdown", async () => {
|
|
268
|
+
recovery.sessionEnded();
|
|
269
|
+
await dropRuntime();
|
|
250
270
|
});
|
|
251
|
-
pi.on("session_shutdown", async () => dropRuntime());
|
|
252
|
-
pi.on("session_before_switch", async () => dropRuntime());
|
|
253
|
-
pi.on("session_before_fork", async () => dropRuntime());
|
|
254
271
|
pi.on("model_select", async (event, ctx) => {
|
|
255
272
|
activeContext = ctx;
|
|
256
273
|
const runtime = activeRuntime;
|
|
@@ -259,33 +276,21 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
259
276
|
if (modelId === undefined || modelId === activeModelId) return;
|
|
260
277
|
activeModelId = modelId;
|
|
261
278
|
const cellManager = activeCells;
|
|
262
|
-
|
|
263
|
-
|
|
279
|
+
const handles = activeHandles;
|
|
280
|
+
if (cellManager === undefined || handles === undefined) return;
|
|
281
|
+
registerEvalForRuntime(runtime, modelId, cellManager, handles);
|
|
264
282
|
});
|
|
265
283
|
// Tool activation has no event of its own; the next turn re-documents a changed contribution set.
|
|
266
284
|
pi.on("turn_start", async () => {
|
|
267
285
|
const runtime = activeRuntime;
|
|
268
286
|
const cellManager = activeCells;
|
|
269
|
-
|
|
287
|
+
const handles = activeHandles;
|
|
288
|
+
if (runtime === undefined || cellManager === undefined || handles === undefined) return;
|
|
270
289
|
if (kernelPreludeDocsKey(promptKernelPreludes(pi)) === promptPreludeDocs) return;
|
|
271
|
-
registerEvalForRuntime(runtime, activeModelId, cellManager);
|
|
290
|
+
registerEvalForRuntime(runtime, activeModelId, cellManager, handles);
|
|
272
291
|
});
|
|
273
292
|
}
|
|
274
293
|
|
|
275
|
-
function hostLine(): string {
|
|
276
|
-
const cpu = os.cpus()[0]?.model?.trim();
|
|
277
|
-
return [`${os.platform()} ${os.arch()}`, cpu, `${os.availableParallelism()} cores`, jsRuntimeLabel()]
|
|
278
|
-
.filter((part): part is string => !!part)
|
|
279
|
-
.join(" \u00b7 ");
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function modelIdFrom(event: unknown): string | undefined {
|
|
283
|
-
if (typeof event !== "object" || event === null || !("model" in event)) return undefined;
|
|
284
|
-
const model = event.model;
|
|
285
|
-
if (typeof model !== "object" || model === null || !("id" in model)) return undefined;
|
|
286
|
-
return typeof model.id === "string" ? model.id : undefined;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
294
|
export {
|
|
290
295
|
KERNEL_TOOLS_CAPABILITIES,
|
|
291
296
|
KERNEL_TOOLS_UNSUPPORTED,
|