@code-yeongyu/senpi-codemode 2026.8.26 → 2026.8.28-2

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 CHANGED
@@ -12,6 +12,62 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.8.28-2] - 2026-08-28
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ ### Fixed
24
+
25
+ ### Removed
26
+
27
+ ## [2026.8.28] - 2026-08-28
28
+
29
+ ### Breaking Changes
30
+
31
+ ### Added
32
+
33
+ ### Changed
34
+
35
+ ### Fixed
36
+
37
+ - JavaScript and Python eval kernels resolve worker and prelude assets from the executable sidecar in Bun-compiled distributions instead of passing unusable `$bunfs` paths to `Worker` and `python3`.
38
+
39
+ ### Removed
40
+
41
+ ## [2026.8.27] - 2026-08-27
42
+
43
+ ### Breaking Changes
44
+
45
+ ### Added
46
+
47
+ ### Changed
48
+
49
+ - Eval tool description examples are now a JS-first mixed set: set up once in JavaScript, fan out batched `Promise.all` session-tool calls in the next cell, then hop to Python when the JS kernel is busy with a detached cell. The detach paragraph now states in the same sentence that another language can continue.
50
+
51
+ ### Fixed
52
+
53
+ - Detached-eval same-language busy errors now name each idle enabled kernel and tell the agent to continue the step there (`continue this step in an idle kernel: js`), instead of only pointing at peek and the output tail. A busy Python kernel no longer reads as "eval is unavailable", which previously sent agents to `bash`+`python3` while JavaScript (or another idle kernel) was free. Single-language sessions and fully-busy sessions omit the idle-kernel claim.
54
+ - JavaScript eval cells now persist only top-level declarations, including destructuring bindings and uninitialized variables, without rewriting declaration-shaped text inside literals or comments.
55
+ - Eval completion and detached-cell handling retain explicit lifecycle observability: nested tool counts, wall/kernel timing, detach state, `peek`, `stop`, hard limits, and crash recovery remain bounded and machine-readable for hosts and telemetry consumers.
56
+
57
+ ### Removed
58
+
59
+ ## [2026.8.26-2] - 2026-08-26
60
+
61
+ ### Breaking Changes
62
+
63
+ ### Added
64
+
65
+ ### Changed
66
+
67
+ ### Fixed
68
+
69
+ ### Removed
70
+
15
71
  ## [2026.8.26] - 2026-08-26
16
72
 
17
73
  ### Breaking Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.8.26",
3
+ "version": "2026.8.28-2",
4
4
  "description": "Source-only senpi extension package for codemode evaluation tools",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -30,14 +30,14 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@babel/parser": "8.0.4",
33
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.26",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.28-2",
34
34
  "typebox": "1.3.18"
35
35
  },
36
36
  "peerDependencies": {
37
- "@code-yeongyu/senpi": "2026.8.26"
37
+ "@code-yeongyu/senpi": "2026.8.28-2"
38
38
  },
39
39
  "devDependencies": {
40
- "@code-yeongyu/senpi": "2026.8.26"
40
+ "@code-yeongyu/senpi": "2026.8.28-2"
41
41
  },
42
42
  "keywords": [
43
43
  "senpi",
@@ -1,5 +1,8 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
1
3
  import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
2
4
  import type { KernelInterruptHandle } from "../../tool/types.ts";
5
+ import { type CodemodeRuntimeAssetEnvironment, resolveCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
3
6
  import { createInlineWorker, type WorkerLike } from "./inline-worker.ts";
4
7
  import {
5
8
  assertJavaScriptKernelOpen,
@@ -16,6 +19,15 @@ import { bridgeError, spawnNodeWorker, WorkerStartupCancelledError, waitForReady
16
19
  export { JavaScriptKernelClosedError, type JavaScriptKernelMode, type JavaScriptRunInput } from "./kernel-contract.ts";
17
20
  export type { JavaScriptKernelOptions } from "./local-module-loader.ts";
18
21
 
22
+ export interface JavaScriptWorkerEntryUrlOptions extends CodemodeRuntimeAssetEnvironment {
23
+ readonly localPath?: string;
24
+ }
25
+
26
+ export function resolveJsWorkerEntryUrl(options: JavaScriptWorkerEntryUrlOptions = {}): URL {
27
+ const localPath = options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "worker-entry.js");
28
+ return pathToFileURL(resolveCodemodeRuntimeAsset(localPath, join("kernels", "js", "worker-entry.js"), options));
29
+ }
30
+
19
31
  export class JavaScriptKernel {
20
32
  readonly #options: JavaScriptKernelOptions;
21
33
  readonly #moduleLoader: LocalModuleLoader;
@@ -161,7 +173,7 @@ export class JavaScriptKernel {
161
173
 
162
174
  #spawnWorker(): WorkerLike {
163
175
  try {
164
- const url = this.#options.workerEntryUrl ?? new URL("./worker-entry.js", import.meta.url);
176
+ const url = this.#options.workerEntryUrl ?? resolveJsWorkerEntryUrl();
165
177
  return spawnNodeWorker(url, this.#options.cwd, this.#options.parallelPoolWidth);
166
178
  } catch (error) {
167
179
  if (!(error instanceof Error)) throw error;
@@ -1,7 +1,21 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
1
3
  import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
4
+ import { type CodemodeRuntimeAssetEnvironment, resolveCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
2
5
  import type { JavaScriptKernelMode } from "./kernel-contract.ts";
3
6
  import { spawnNodeWorker } from "./worker-host.ts";
4
7
 
8
+ export interface JavaScriptInlineWorkerEntryUrlOptions extends CodemodeRuntimeAssetEnvironment {
9
+ readonly localPath?: string;
10
+ }
11
+
12
+ export function resolveInlineWorkerEntryUrl(options: JavaScriptInlineWorkerEntryUrlOptions = {}): URL {
13
+ const localPath = options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "inline-worker-entry.js");
14
+ return pathToFileURL(
15
+ resolveCodemodeRuntimeAsset(localPath, join("kernels", "js", "inline-worker-entry.js"), options),
16
+ );
17
+ }
18
+
5
19
  export interface WorkerLike {
6
20
  readonly mode: JavaScriptKernelMode;
7
21
  postMessage(message: HostToKernelMessage): void;
@@ -11,5 +25,5 @@ export interface WorkerLike {
11
25
  }
12
26
 
13
27
  export function createInlineWorker(cwd: string, parallelPoolWidth: number): WorkerLike {
14
- return spawnNodeWorker(new URL("./inline-worker-entry.js", import.meta.url), cwd, parallelPoolWidth, "inline");
28
+ return spawnNodeWorker(resolveInlineWorkerEntryUrl(), cwd, parallelPoolWidth, "inline");
15
29
  }
@@ -10,11 +10,721 @@ export async function awaitMaybePromise(value) {
10
10
  }
11
11
 
12
12
  export function wrapUserCode(code) {
13
- const persistentCode = code.replace(/(^|\n)\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/gu, "$1globalThis.$2 =");
13
+ const persistentCode = persistTopLevelDeclarations(code);
14
14
  if (/\breturn\b/u.test(persistentCode)) return `(async () => {\n${persistentCode}\n})()`;
15
15
  return `(async () => {\n${captureLastExpression(persistentCode)}\n})()`;
16
16
  }
17
17
 
18
+ const IDENTIFIER_START_RE = /[$_\p{ID_Start}]/u;
19
+ const IDENTIFIER_CONTINUE_RE = /[$_\p{ID_Continue}\u200c\u200d]/u;
20
+ const IDENTIFIER_RE = /^[\p{ID_Start}$_][\p{ID_Continue}\u200c\u200d]*$/u;
21
+ const DECLARATION_KEYWORDS = new Set(["const", "let", "var"]);
22
+ const REGEX_PREFIX_KEYWORDS = new Set([
23
+ "await",
24
+ "case",
25
+ "delete",
26
+ "do",
27
+ "else",
28
+ "in",
29
+ "instanceof",
30
+ "new",
31
+ "of",
32
+ "return",
33
+ "throw",
34
+ "typeof",
35
+ "void",
36
+ "yield",
37
+ ]);
38
+ const EXPRESSION_PREFIX_KEYWORDS = new Set(["await", "delete", "new", "throw", "typeof", "void", "yield"]);
39
+
40
+ function persistTopLevelDeclarations(code) {
41
+ const edits = [];
42
+ let round = 0;
43
+ let square = 0;
44
+ let curly = 0;
45
+ let statementStart = true;
46
+ let canStartRegex = true;
47
+
48
+ for (let index = 0; index < code.length; index += 1) {
49
+ const char = code[index];
50
+ const next = code[index + 1];
51
+ if (char === "/" && next === "/") {
52
+ index = skipLineComment(code, index) - 1;
53
+ continue;
54
+ }
55
+ if (char === "/" && next === "*") {
56
+ index = skipBlockComment(code, index) - 1;
57
+ continue;
58
+ }
59
+ if (char === "'" || char === '"') {
60
+ index = skipQuotedLiteral(code, index) - 1;
61
+ statementStart = false;
62
+ canStartRegex = false;
63
+ continue;
64
+ }
65
+ if (char === "`") {
66
+ index = skipTemplateLiteral(code, index) - 1;
67
+ statementStart = false;
68
+ canStartRegex = false;
69
+ continue;
70
+ }
71
+ if (char === "/" && canStartRegex) {
72
+ index = skipRegexLiteral(code, index) - 1;
73
+ statementStart = false;
74
+ canStartRegex = false;
75
+ continue;
76
+ }
77
+ if (isIdentifierStart(char)) {
78
+ const end = readIdentifier(code, index);
79
+ const token = code.slice(index, end);
80
+ if (round === 0 && square === 0 && curly === 0 && statementStart && DECLARATION_KEYWORDS.has(token)) {
81
+ const declarationEnd = findDeclarationEnd(code, end);
82
+ const replacement = rewriteDeclaration(code, index, end, declarationEnd, token);
83
+ if (replacement !== undefined) edits.push({ start: index, end: declarationEnd, text: replacement });
84
+ }
85
+ statementStart = false;
86
+ canStartRegex = REGEX_PREFIX_KEYWORDS.has(token);
87
+ index = end - 1;
88
+ continue;
89
+ }
90
+ if (isDecimalDigit(char)) {
91
+ index = skipNumberLiteral(code, index) - 1;
92
+ statementStart = false;
93
+ canStartRegex = false;
94
+ continue;
95
+ }
96
+ if (char === "(") {
97
+ round += 1;
98
+ statementStart = false;
99
+ canStartRegex = true;
100
+ continue;
101
+ }
102
+ if (char === ")") {
103
+ round = Math.max(0, round - 1);
104
+ statementStart = false;
105
+ canStartRegex = false;
106
+ continue;
107
+ }
108
+ if (char === "[") {
109
+ square += 1;
110
+ statementStart = false;
111
+ canStartRegex = true;
112
+ continue;
113
+ }
114
+ if (char === "]") {
115
+ square = Math.max(0, square - 1);
116
+ statementStart = false;
117
+ canStartRegex = false;
118
+ continue;
119
+ }
120
+ if (char === "{") {
121
+ curly += 1;
122
+ statementStart = false;
123
+ canStartRegex = true;
124
+ continue;
125
+ }
126
+ if (char === "}") {
127
+ curly = Math.max(0, curly - 1);
128
+ statementStart = curly === 0 && round === 0 && square === 0;
129
+ canStartRegex = false;
130
+ continue;
131
+ }
132
+ if (char === ";") {
133
+ statementStart = round === 0 && square === 0 && curly === 0;
134
+ canStartRegex = true;
135
+ continue;
136
+ }
137
+ if (isLineTerminator(char) && round === 0 && square === 0 && curly === 0) {
138
+ statementStart = true;
139
+ canStartRegex = true;
140
+ continue;
141
+ }
142
+ if (!/\s/u.test(char)) {
143
+ statementStart = false;
144
+ canStartRegex = isExpressionOperator(char);
145
+ }
146
+ }
147
+
148
+ return applyTextEdits(code, edits);
149
+ }
150
+
151
+ function findDeclarationEnd(code, start) {
152
+ let round = 0;
153
+ let square = 0;
154
+ let curly = 0;
155
+ let canEnd = false;
156
+ let canStartRegex = true;
157
+
158
+ for (let index = start; index < code.length; index += 1) {
159
+ const char = code[index];
160
+ const next = code[index + 1];
161
+ if (char === "/" && next === "/") {
162
+ index = skipLineComment(code, index) - 1;
163
+ continue;
164
+ }
165
+ if (char === "/" && next === "*") {
166
+ index = skipBlockComment(code, index) - 1;
167
+ continue;
168
+ }
169
+ if (char === "'" || char === '"') {
170
+ index = skipQuotedLiteral(code, index) - 1;
171
+ canEnd = true;
172
+ canStartRegex = false;
173
+ continue;
174
+ }
175
+ if (char === "`") {
176
+ index = skipTemplateLiteral(code, index) - 1;
177
+ canEnd = true;
178
+ canStartRegex = false;
179
+ continue;
180
+ }
181
+ if (char === "/" && canStartRegex) {
182
+ index = skipRegexLiteral(code, index) - 1;
183
+ canEnd = true;
184
+ canStartRegex = false;
185
+ continue;
186
+ }
187
+ if (isIdentifierStart(char)) {
188
+ const end = readIdentifier(code, index);
189
+ const token = code.slice(index, end);
190
+ canEnd = !EXPRESSION_PREFIX_KEYWORDS.has(token);
191
+ canStartRegex = REGEX_PREFIX_KEYWORDS.has(token);
192
+ index = end - 1;
193
+ continue;
194
+ }
195
+ if (isDecimalDigit(char)) {
196
+ index = skipNumberLiteral(code, index) - 1;
197
+ canEnd = true;
198
+ canStartRegex = false;
199
+ continue;
200
+ }
201
+ if (char === "(") {
202
+ round += 1;
203
+ canEnd = false;
204
+ canStartRegex = true;
205
+ continue;
206
+ }
207
+ if (char === ")") {
208
+ round = Math.max(0, round - 1);
209
+ canEnd = true;
210
+ canStartRegex = false;
211
+ continue;
212
+ }
213
+ if (char === "[") {
214
+ square += 1;
215
+ canEnd = false;
216
+ canStartRegex = true;
217
+ continue;
218
+ }
219
+ if (char === "]") {
220
+ square = Math.max(0, square - 1);
221
+ canEnd = true;
222
+ canStartRegex = false;
223
+ continue;
224
+ }
225
+ if (char === "{") {
226
+ curly += 1;
227
+ canEnd = false;
228
+ canStartRegex = true;
229
+ continue;
230
+ }
231
+ if (char === "}") {
232
+ curly = Math.max(0, curly - 1);
233
+ canEnd = true;
234
+ canStartRegex = false;
235
+ continue;
236
+ }
237
+ if (char === ";" && round === 0 && square === 0 && curly === 0) return index + 1;
238
+ if (char === "," && round === 0 && square === 0 && curly === 0) {
239
+ canEnd = false;
240
+ canStartRegex = true;
241
+ continue;
242
+ }
243
+ if (isLineTerminator(char) && round === 0 && square === 0 && curly === 0 && canEnd) {
244
+ const nextIndex = nextSignificantIndex(code, index + 1);
245
+ if (nextIndex >= code.length || !isDeclarationContinuation(code[nextIndex])) return index;
246
+ }
247
+ if (!/\s/u.test(char)) {
248
+ canEnd = false;
249
+ canStartRegex = isExpressionOperator(char);
250
+ }
251
+ }
252
+ return code.length;
253
+ }
254
+
255
+ function rewriteDeclaration(code, declarationStart, start, end, keyword) {
256
+ const source = code.slice(declarationStart, end);
257
+ const assignments = [];
258
+ const preserveDeclaration = source.includes("//") || source.includes("/*");
259
+ for (const [segmentStart, segmentEnd] of splitDeclarators(code, start, end)) {
260
+ const segment = code.slice(segmentStart, segmentEnd);
261
+ if (!segment.trim()) continue;
262
+ const initializerStart = findTopLevelEquals(segment);
263
+ if (initializerStart < 0 && keyword === "const") return undefined;
264
+ const pattern = trimPattern(initializerStart < 0 ? segment : segment.slice(0, initializerStart));
265
+ const bindings = [];
266
+ collectPatternNames(pattern, bindings);
267
+ if (bindings.length === 0) return undefined;
268
+ const target = rewriteBindingPattern(pattern);
269
+ if (target === undefined) return undefined;
270
+ const initializer = initializerStart < 0 ? "undefined" : segment.slice(initializerStart + 1).trim();
271
+ const [assignmentInitializer, comment] = splitTrailingLineComment(initializer);
272
+ const assignmentComment = preserveDeclaration ? "" : comment;
273
+ assignments.push(
274
+ `${target.startsWith("{") || target.startsWith("[") ? `(${target} = ${assignmentInitializer})` : `${target} = ${assignmentInitializer}`};${assignmentComment}`,
275
+ );
276
+ }
277
+ if (assignments.length === 0) return undefined;
278
+ return preserveDeclaration ? `${source}\n${assignments.join("\n")}` : assignments.join("\n");
279
+ }
280
+
281
+ function rewriteBindingPattern(source) {
282
+ const pattern = trimPattern(source);
283
+ if (!pattern) return undefined;
284
+ const defaultStart = findTopLevelEquals(pattern);
285
+ if (defaultStart >= 0) {
286
+ const target = rewriteBindingPattern(pattern.slice(0, defaultStart));
287
+ return target === undefined ? undefined : `${target} = ${pattern.slice(defaultStart + 1).trim()}`;
288
+ }
289
+ if (IDENTIFIER_RE.test(pattern)) return `globalThis[${JSON.stringify(pattern)}]`;
290
+ if (pattern.startsWith("{") && pattern.endsWith("}")) {
291
+ const properties = splitPatternElements(pattern.slice(1, -1)).map((property) => rewriteObjectBinding(property));
292
+ if (properties.some((property) => property === undefined)) return undefined;
293
+ return `{${properties.filter((property) => property !== undefined).join(", ")}}`;
294
+ }
295
+ if (pattern.startsWith("[") && pattern.endsWith("]")) {
296
+ const elements = splitPatternElements(pattern.slice(1, -1)).map((element) => {
297
+ if (!element.trim()) return "";
298
+ const rest = trimPattern(element).startsWith("...");
299
+ const target = rewriteBindingPattern(rest ? trimPattern(element).slice(3) : element);
300
+ return target === undefined ? undefined : rest ? `...${target}` : target;
301
+ });
302
+ if (elements.some((element) => element === undefined)) return undefined;
303
+ return `[${elements.join(", ")}]`;
304
+ }
305
+ return undefined;
306
+ }
307
+
308
+ function rewriteObjectBinding(source) {
309
+ const property = trimPattern(source);
310
+ if (!property) return "";
311
+ if (property.startsWith("...")) {
312
+ const target = rewriteBindingPattern(property.slice(3));
313
+ return target === undefined ? undefined : `...${target}`;
314
+ }
315
+ const initializerStart = findTopLevelEquals(property);
316
+ const colon = findTopLevelColon(property);
317
+ if (colon >= 0 && (initializerStart < 0 || colon < initializerStart)) {
318
+ const target = rewriteBindingPattern(property.slice(colon + 1));
319
+ return target === undefined ? undefined : `${property.slice(0, colon + 1).trim()} ${target}`;
320
+ }
321
+ if (initializerStart >= 0) {
322
+ const target = rewriteBindingPattern(property.slice(0, initializerStart));
323
+ return target === undefined ? undefined : `${property.slice(0, initializerStart).trim()}: ${target} = ${property.slice(initializerStart + 1).trim()}`;
324
+ }
325
+ const target = rewriteBindingPattern(property);
326
+ return target === undefined ? undefined : `${property}: ${target}`;
327
+ }
328
+
329
+ function collectPatternNames(source, bindings) {
330
+ const pattern = trimPattern(source);
331
+ if (!pattern) return;
332
+ const defaultStart = findTopLevelEquals(pattern);
333
+ if (defaultStart >= 0) {
334
+ collectPatternNames(pattern.slice(0, defaultStart), bindings);
335
+ return;
336
+ }
337
+ if (pattern.startsWith("{") && pattern.endsWith("}")) {
338
+ for (const property of splitPatternElements(pattern.slice(1, -1))) {
339
+ const trimmed = trimPattern(property);
340
+ if (!trimmed) continue;
341
+ if (trimmed.startsWith("...")) {
342
+ collectPatternNames(trimmed.slice(3), bindings);
343
+ continue;
344
+ }
345
+ const equals = findTopLevelEquals(trimmed);
346
+ const colon = findTopLevelColon(trimmed);
347
+ if (colon >= 0 && (equals < 0 || colon < equals)) collectPatternNames(trimmed.slice(colon + 1), bindings);
348
+ else collectPatternNames(equals < 0 ? trimmed : trimmed.slice(0, equals), bindings);
349
+ }
350
+ return;
351
+ }
352
+ if (pattern.startsWith("[") && pattern.endsWith("]")) {
353
+ for (const element of splitPatternElements(pattern.slice(1, -1))) {
354
+ const trimmed = trimPattern(element);
355
+ if (!trimmed) continue;
356
+ collectPatternNames(trimmed.startsWith("...") ? trimmed.slice(3) : trimmed, bindings);
357
+ }
358
+ return;
359
+ }
360
+ const equals = findTopLevelEquals(pattern);
361
+ const name = trimPattern(equals < 0 ? pattern : pattern.slice(0, equals));
362
+ if (IDENTIFIER_RE.test(name)) bindings.push(name);
363
+ }
364
+
365
+ function splitDeclarators(code, start, end) {
366
+ const ranges = [];
367
+ let segmentStart = start;
368
+ let round = 0;
369
+ let square = 0;
370
+ let curly = 0;
371
+ let canStartRegex = true;
372
+ for (let index = start; index < end; index += 1) {
373
+ const char = code[index];
374
+ const next = code[index + 1];
375
+ if (char === "/" && next === "/") {
376
+ index = Math.min(end, skipLineComment(code, index)) - 1;
377
+ continue;
378
+ }
379
+ if (char === "/" && next === "*") {
380
+ index = Math.min(end, skipBlockComment(code, index)) - 1;
381
+ continue;
382
+ }
383
+ if (char === "'" || char === '"') {
384
+ index = Math.min(end, skipQuotedLiteral(code, index)) - 1;
385
+ canStartRegex = false;
386
+ continue;
387
+ }
388
+ if (char === "`") {
389
+ index = Math.min(end, skipTemplateLiteral(code, index)) - 1;
390
+ canStartRegex = false;
391
+ continue;
392
+ }
393
+ if (char === "/" && canStartRegex) {
394
+ index = Math.min(end, skipRegexLiteral(code, index)) - 1;
395
+ canStartRegex = false;
396
+ continue;
397
+ }
398
+ if (char === "(") round += 1;
399
+ else if (char === ")") round = Math.max(0, round - 1);
400
+ else if (char === "[") square += 1;
401
+ else if (char === "]") square = Math.max(0, square - 1);
402
+ else if (char === "{") curly += 1;
403
+ else if (char === "}") curly = Math.max(0, curly - 1);
404
+ else if (char === "," && round === 0 && square === 0 && curly === 0) {
405
+ ranges.push([segmentStart, index]);
406
+ segmentStart = index + 1;
407
+ }
408
+ if (!/\s/u.test(char)) canStartRegex = isExpressionOperator(char) || char === "(" || char === "[" || char === "{";
409
+ }
410
+ if (segmentStart < end && code.slice(segmentStart, end).trim() !== ";") ranges.push([segmentStart, end - (code[end - 1] === ";" ? 1 : 0)]);
411
+ return ranges;
412
+ }
413
+
414
+ function splitPatternElements(source) {
415
+ const elements = [];
416
+ let start = 0;
417
+ let round = 0;
418
+ let square = 0;
419
+ let curly = 0;
420
+ for (let index = 0; index < source.length; index += 1) {
421
+ const char = source[index];
422
+ const next = source[index + 1];
423
+ if (char === "/" && next === "/") {
424
+ index = skipLineComment(source, index) - 1;
425
+ continue;
426
+ }
427
+ if (char === "/" && next === "*") {
428
+ index = skipBlockComment(source, index) - 1;
429
+ continue;
430
+ }
431
+ if (char === "'" || char === '"') {
432
+ index = skipQuotedLiteral(source, index) - 1;
433
+ continue;
434
+ }
435
+ if (char === "`") {
436
+ index = skipTemplateLiteral(source, index) - 1;
437
+ continue;
438
+ }
439
+ if (char === "(") round += 1;
440
+ else if (char === ")") round = Math.max(0, round - 1);
441
+ else if (char === "[") square += 1;
442
+ else if (char === "]") square = Math.max(0, square - 1);
443
+ else if (char === "{") curly += 1;
444
+ else if (char === "}") curly = Math.max(0, curly - 1);
445
+ else if (char === "," && round === 0 && square === 0 && curly === 0) {
446
+ elements.push(source.slice(start, index));
447
+ start = index + 1;
448
+ }
449
+ }
450
+ elements.push(source.slice(start));
451
+ return elements;
452
+ }
453
+
454
+ function findTopLevelEquals(source) {
455
+ return findTopLevelCharacter(source, "=");
456
+ }
457
+
458
+ function findTopLevelColon(source) {
459
+ return findTopLevelCharacter(source, ":");
460
+ }
461
+
462
+ function findTopLevelCharacter(source, target) {
463
+ let round = 0;
464
+ let square = 0;
465
+ let curly = 0;
466
+ for (let index = 0; index < source.length; index += 1) {
467
+ const char = source[index];
468
+ const next = source[index + 1];
469
+ if (char === "/" && next === "/") {
470
+ index = skipLineComment(source, index) - 1;
471
+ continue;
472
+ }
473
+ if (char === "/" && next === "*") {
474
+ index = skipBlockComment(source, index) - 1;
475
+ continue;
476
+ }
477
+ if (char === "'" || char === '"') {
478
+ index = skipQuotedLiteral(source, index) - 1;
479
+ continue;
480
+ }
481
+ if (char === "`") {
482
+ index = skipTemplateLiteral(source, index) - 1;
483
+ continue;
484
+ }
485
+ if (char === "(") round += 1;
486
+ else if (char === ")") round = Math.max(0, round - 1);
487
+ else if (char === "[") square += 1;
488
+ else if (char === "]") square = Math.max(0, square - 1);
489
+ else if (char === "{") curly += 1;
490
+ else if (char === "}") curly = Math.max(0, curly - 1);
491
+ else if (char === target && round === 0 && square === 0 && curly === 0) return index;
492
+ }
493
+ return -1;
494
+ }
495
+
496
+ function applyTextEdits(code, edits) {
497
+ let output = code;
498
+ for (const edit of edits.toSorted((left, right) => right.start - left.start)) {
499
+ output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
500
+ }
501
+ return output;
502
+ }
503
+
504
+ function splitTrailingLineComment(source) {
505
+ let canStartRegex = true;
506
+ for (let index = 0; index < source.length; index += 1) {
507
+ const char = source[index];
508
+ const next = source[index + 1];
509
+ if (char === "/" && next === "/") return [source.slice(0, index).trimEnd(), source.slice(index)];
510
+ if (char === "/" && next === "*") {
511
+ index = skipBlockComment(source, index) - 1;
512
+ continue;
513
+ }
514
+ if (char === "'" || char === '"') {
515
+ index = skipQuotedLiteral(source, index) - 1;
516
+ canStartRegex = false;
517
+ continue;
518
+ }
519
+ if (char === "`") {
520
+ index = skipTemplateLiteral(source, index) - 1;
521
+ canStartRegex = false;
522
+ continue;
523
+ }
524
+ if (char === "/" && canStartRegex) {
525
+ index = skipRegexLiteral(source, index) - 1;
526
+ canStartRegex = false;
527
+ continue;
528
+ }
529
+ if (isIdentifierStart(char)) {
530
+ const end = readIdentifier(source, index);
531
+ canStartRegex = REGEX_PREFIX_KEYWORDS.has(source.slice(index, end));
532
+ index = end - 1;
533
+ continue;
534
+ }
535
+ if (isDecimalDigit(char)) {
536
+ index = skipNumberLiteral(source, index) - 1;
537
+ canStartRegex = false;
538
+ continue;
539
+ }
540
+ if (!/\s/u.test(char)) canStartRegex = isExpressionOperator(char) || char === "(" || char === "[" || char === "{";
541
+ }
542
+ return [source, ""];
543
+ }
544
+
545
+ function trimPattern(source) {
546
+ let start = 0;
547
+ let end = source.length;
548
+ while (start < end) {
549
+ if (/\s/u.test(source[start])) {
550
+ start += 1;
551
+ continue;
552
+ }
553
+ if (source.startsWith("//", start)) {
554
+ start = skipLineComment(source, start);
555
+ continue;
556
+ }
557
+ if (source.startsWith("/*", start)) {
558
+ start = skipBlockComment(source, start);
559
+ continue;
560
+ }
561
+ break;
562
+ }
563
+ while (true) {
564
+ while (end > start && /\s/u.test(source[end - 1])) end -= 1;
565
+ if (end < start + 2 || source.slice(end - 2, end) !== "*/") break;
566
+ const commentStart = source.lastIndexOf("/*", end - 2);
567
+ if (commentStart < start) break;
568
+ end = commentStart;
569
+ }
570
+ return source.slice(start, end);
571
+ }
572
+
573
+ function nextSignificantIndex(code, start) {
574
+ for (let index = start; index < code.length; index += 1) {
575
+ if (/\s/u.test(code[index])) continue;
576
+ if (code.startsWith("//", index)) {
577
+ index = skipLineComment(code, index) - 1;
578
+ continue;
579
+ }
580
+ if (code.startsWith("/*", index)) {
581
+ index = skipBlockComment(code, index) - 1;
582
+ continue;
583
+ }
584
+ return index;
585
+ }
586
+ return code.length;
587
+ }
588
+
589
+ function isDeclarationContinuation(char) {
590
+ return char !== undefined && /[.[(,+\-*/%&|^?:<>=!~]/u.test(char);
591
+ }
592
+
593
+ function isExpressionOperator(char) {
594
+ return /[=,+\-*/%&|^?:<>!~]/u.test(char);
595
+ }
596
+
597
+ function isIdentifierStart(char) {
598
+ return char !== undefined && IDENTIFIER_START_RE.test(char);
599
+ }
600
+
601
+ function readIdentifier(code, start) {
602
+ let index = start + 1;
603
+ while (index < code.length && IDENTIFIER_CONTINUE_RE.test(code[index])) index += 1;
604
+ return index;
605
+ }
606
+
607
+ function isDecimalDigit(char) {
608
+ return char !== undefined && /[0-9]/u.test(char);
609
+ }
610
+
611
+ function skipNumberLiteral(code, start) {
612
+ let index = start + 1;
613
+ while (index < code.length && /[0-9A-Fa-f_xXn.eE]/u.test(code[index])) index += 1;
614
+ return index;
615
+ }
616
+
617
+ function skipQuotedLiteral(code, start) {
618
+ const quote = code[start];
619
+ for (let index = start + 1; index < code.length; index += 1) {
620
+ if (code[index] === "\\") {
621
+ index += 1;
622
+ continue;
623
+ }
624
+ if (code[index] === quote) return index + 1;
625
+ }
626
+ return code.length;
627
+ }
628
+
629
+ function skipTemplateLiteral(code, start) {
630
+ for (let index = start + 1; index < code.length; index += 1) {
631
+ const char = code[index];
632
+ if (char === "\\") {
633
+ index += 1;
634
+ continue;
635
+ }
636
+ if (char === "`") return index + 1;
637
+ if (char === "$" && code[index + 1] === "{") {
638
+ index = skipTemplateExpression(code, index + 2) - 1;
639
+ }
640
+ }
641
+ return code.length;
642
+ }
643
+
644
+ function skipTemplateExpression(code, start) {
645
+ let curly = 1;
646
+ let canStartRegex = true;
647
+ for (let index = start; index < code.length; index += 1) {
648
+ const char = code[index];
649
+ const next = code[index + 1];
650
+ if (char === "/" && next === "/") {
651
+ index = skipLineComment(code, index) - 1;
652
+ continue;
653
+ }
654
+ if (char === "/" && next === "*") {
655
+ index = skipBlockComment(code, index) - 1;
656
+ continue;
657
+ }
658
+ if (char === "'" || char === '"') {
659
+ index = skipQuotedLiteral(code, index) - 1;
660
+ canStartRegex = false;
661
+ continue;
662
+ }
663
+ if (char === "`") {
664
+ index = skipTemplateLiteral(code, index) - 1;
665
+ canStartRegex = false;
666
+ continue;
667
+ }
668
+ if (char === "/" && canStartRegex) {
669
+ index = skipRegexLiteral(code, index) - 1;
670
+ canStartRegex = false;
671
+ continue;
672
+ }
673
+ if (isIdentifierStart(char)) {
674
+ const end = readIdentifier(code, index);
675
+ canStartRegex = REGEX_PREFIX_KEYWORDS.has(code.slice(index, end));
676
+ index = end - 1;
677
+ continue;
678
+ }
679
+ if (char === "{") curly += 1;
680
+ else if (char === "}" && --curly === 0) return index + 1;
681
+ canStartRegex = char === "(" || char === "[" || char === "{" || isExpressionOperator(char) || char === ",";
682
+ }
683
+ return code.length;
684
+ }
685
+
686
+ function skipRegexLiteral(code, start) {
687
+ let inClass = false;
688
+ for (let index = start + 1; index < code.length; index += 1) {
689
+ const char = code[index];
690
+ if (char === "\\") {
691
+ index += 1;
692
+ continue;
693
+ }
694
+ if (char === "[") {
695
+ inClass = true;
696
+ continue;
697
+ }
698
+ if (char === "]") {
699
+ inClass = false;
700
+ continue;
701
+ }
702
+ if (char === "/" && !inClass) {
703
+ let end = index + 1;
704
+ while (end < code.length && /[A-Za-z]/u.test(code[end])) end += 1;
705
+ return end;
706
+ }
707
+ if (isLineTerminator(char)) return index;
708
+ }
709
+ return code.length;
710
+ }
711
+
712
+ function skipLineComment(code, start) {
713
+ for (let index = start + 2; index < code.length; index += 1) {
714
+ if (isLineTerminator(code[index])) return index;
715
+ }
716
+ return code.length;
717
+ }
718
+
719
+ function isLineTerminator(char) {
720
+ return char === "\n" || char === "\r" || char === "\u2028" || char === "\u2029";
721
+ }
722
+
723
+ function skipBlockComment(code, start) {
724
+ const end = code.indexOf("*/", start + 2);
725
+ return end < 0 ? code.length : end + 2;
726
+ }
727
+
18
728
  function captureLastExpression(code) {
19
729
  const start = findLastTopLevelStatementStart(code);
20
730
  const head = code.slice(0, start);
@@ -8,6 +8,7 @@ import {
8
8
  isKernelToHostMessage,
9
9
  type KernelToHostMessage,
10
10
  } from "../../bridge/protocol.ts";
11
+ import { type CodemodeRuntimeAssetEnvironment, resolveCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
11
12
  import {
12
13
  defaultSpawn,
13
14
  hardKill,
@@ -47,6 +48,18 @@ export interface PythonTransportOptions {
47
48
 
48
49
  const hardKillWaitMs = 500;
49
50
 
51
+ export interface PythonPreludePathOptions extends CodemodeRuntimeAssetEnvironment {
52
+ readonly localPath?: string;
53
+ }
54
+
55
+ export function resolvePythonPreludePath(options: PythonPreludePathOptions = {}): string {
56
+ return resolveCodemodeRuntimeAsset(
57
+ options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "prelude.py"),
58
+ join("kernels", "py", "prelude.py"),
59
+ options,
60
+ );
61
+ }
62
+
50
63
  export class PythonKernelTransport {
51
64
  readonly #options: PythonTransportOptions;
52
65
  readonly #child: KernelChild;
@@ -64,7 +77,7 @@ export class PythonKernelTransport {
64
77
  }
65
78
 
66
79
  static async start(options: PythonTransportOptions): Promise<PythonKernelTransport> {
67
- const scriptPath = join(dirname(fileURLToPath(import.meta.url)), "prelude.py");
80
+ const scriptPath = resolvePythonPreludePath();
68
81
  const invocation = splitCommand(options.interpreterPath);
69
82
  const spawnOptions: KernelSpawnOptions = {
70
83
  command: invocation.command,
@@ -70,21 +70,21 @@ type EvalPromptExample = {
70
70
  const REUSE_CHAIN_EXAMPLES = [
71
71
  {
72
72
  caption: "First call — set up once",
73
- language: "py",
73
+ language: "js",
74
74
  summary: "Count all TypeScript source files under src/ excluding tests",
75
- code: "from pathlib import Path\nfrom collections import Counter\nfiles = [p for p in Path('src').rglob('*.ts') if 'test' not in p.parts]\nprint(len(files))",
75
+ code: "import { readdir } from 'node:fs/promises'\nimport { extname } from 'node:path'\nconst files = (await readdir('src', { recursive: true })).filter(f => extname(f) === '.ts' && !f.includes('test'))\nprint(files.length)",
76
76
  },
77
77
  {
78
- caption: "Second call — reuse `files`, batch-read in one cell",
79
- language: "py",
80
- summary: "Find which files reference legacyClient so we know what to migrate",
81
- code: "hits = Counter()\nfor p in files:\n hits[p.name] = read(p).count('legacyClient')\ndisplay({k: v for k, v in hits.items() if v})",
78
+ caption: "Second call — reuse `files`, fan out session tools in parallel",
79
+ language: "js",
80
+ summary: "Grep legacyClient per directory in one cell",
81
+ code: "const dirs = [...new Set(files.map(f => f.split('/')[0]))]\nconst hits = await Promise.all(dirs.map(d => tool.grep({ pattern: 'legacyClient', path: d })))\ndisplay(hits.map(h => h.matches?.length ?? 0))",
82
82
  },
83
83
  {
84
- caption: "Third call reuse results, fan out session tools in parallel",
84
+ caption: "JS kernel is busy with a detached cell continue in py",
85
85
  language: "py",
86
- summary: "Confirm exact callsite lines in each directory to plan the refactor",
87
- code: "dirs = ['src/core', 'src/tools']\ndisplay(parallel([lambda d=d: tool.grep({'pattern': 'legacyClient', 'path': d}) for d in dirs]))",
86
+ summary: "Aggregate legacyClient hits while JS is busy",
87
+ code: "from pathlib import Path\nprint(sum('legacyClient' in read(p) for p in Path('src').rglob('*.ts')))",
88
88
  },
89
89
  ] as const satisfies readonly EvalPromptExample[];
90
90
 
@@ -129,7 +129,7 @@ Fields:
129
129
  - \`reset\` (optional) — wipe this language's kernel first.{{#ifAll py js}} Per-language: a \`py\` reset never touches the JS VM.{{/ifAll}}
130
130
  - \`action\` (optional) — defaults to \`"run"\`. A detached cell returns its id: use \`eval({ action: "peek", cell_id })\` for buffered output/state or \`eval({ action: "stop", cell_id })\` to cancel it.
131
131
 
132
- A detached cell keeps its language kernel busy while it finishes. Do not re-run a detached cell: the same-language busy error names its cell id and output tail; another language can continue. Completion arrives as one notification with the final value/error and buffered output. Stopping a cell interrupts its kernel; the stop result states whether kernel state survived or the kernel was restarted and its variables lost.
132
+ A detached cell keeps its language kernel busy while it finishes; another language can continue. Do not re-run a detached cell: the same-language busy error names its cell id and output tail. Completion arrives as one notification with the final value/error and buffered output. Stopping a cell interrupts its kernel; the stop result states whether kernel state survived or the kernel was restarted and its variables lost.
133
133
 
134
134
  {{#if py}}Live event loop: use top-level \`await\` directly; \`asyncio.run(…)\` raises "cannot be called from a running event loop".{{/if}}
135
135
  {{#if js}}JS runs under Node.js worker: top-level \`await\`/\`return\` work; \`fetch\`/\`Buffer\` available.{{/if}}
@@ -5,7 +5,7 @@ import type {
5
5
  EvalDetachedCellState,
6
6
  } from "./detached-cell-manager.ts";
7
7
  import { interruptionStateNote } from "./interrupt-note.ts";
8
- import type { EvalCellResult, EvalControlInput, EvalToolDetails, EvalToolInput } from "./types.ts";
8
+ import type { EvalCellResult, EvalControlInput, EvalLanguage, EvalToolDetails, EvalToolInput } from "./types.ts";
9
9
 
10
10
  export async function executeEvalControl(
11
11
  cellManager: EvalDetachedCellManager,
@@ -101,10 +101,16 @@ export function resultForDetachedState(
101
101
  };
102
102
  }
103
103
 
104
- export function detachedKernelBusyError(snapshot: EvalDetachedCellSnapshot): Error {
104
+ export function detachedKernelBusyError(
105
+ snapshot: EvalDetachedCellSnapshot,
106
+ idleLanguages: readonly EvalLanguage[] = [],
107
+ ): Error {
105
108
  const tail = snapshot.outputTail.length === 0 ? "(no output yet)" : snapshot.outputTail;
109
+ const peek = `eval({ action: "peek", cell_id: "${snapshot.cellId}" })`;
110
+ const idleHint =
111
+ idleLanguages.length === 0 ? "" : ` or continue this step in an idle kernel: ${idleLanguages.join(", ")}`;
106
112
  return new Error(
107
- `The ${snapshot.language} eval kernel is busy running detached cell ${snapshot.cellId}. Do not re-run it; use eval({ action: "peek", cell_id: "${snapshot.cellId}" }). Current output tail:\n${tail}`,
113
+ `The ${snapshot.language} eval kernel is busy running detached cell ${snapshot.cellId} - peek with ${peek}${idleHint}. Do not re-run the busy cell. Current output tail:\n${tail}`,
108
114
  );
109
115
  }
110
116
 
@@ -67,7 +67,12 @@ export function createEvalTool(options: CreateEvalToolOptions): ToolDefinition<E
67
67
  `Unsupported eval language "${request.language}". Enabled languages: ${languages.join(", ")}`,
68
68
  );
69
69
  const busy = cellManager.busyFor(request.language);
70
- if (busy !== undefined) throw detachedKernelBusyError(busy);
70
+ if (busy !== undefined) {
71
+ const idleLanguages = languages.filter(
72
+ (language) => language !== request.language && cellManager.busyFor(language) === undefined,
73
+ );
74
+ throw detachedKernelBusyError(busy, idleLanguages);
75
+ }
71
76
  options.executionTracker?.assertEvalExecutionAllowed();
72
77
  const lifecycleController = new AbortController();
73
78
  const combinedSignal = signal