@code-yeongyu/senpi-codemode 2026.10.5 → 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.
Files changed (68) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +20 -2
  3. package/package.json +4 -4
  4. package/src/bridge/kernel-tools-protocol.ts +1 -1
  5. package/src/bridge/reserved.ts +10 -0
  6. package/src/bridges/eval-virtual-schemas.ts +79 -0
  7. package/src/bridges/handle-bridge.ts +72 -0
  8. package/src/bridges/reserved-dispatch.ts +20 -2
  9. package/src/bridges/schema-bridge.ts +4 -0
  10. package/src/completion/handle.ts +69 -0
  11. package/src/completion/tool-bridge.ts +31 -12
  12. package/src/config/feature-settings.ts +110 -0
  13. package/src/config/memory-settings.ts +8 -0
  14. package/src/config/settings-constants.ts +7 -0
  15. package/src/config/settings-overrides.ts +72 -0
  16. package/src/config/settings.ts +35 -78
  17. package/src/config/unknown-settings.ts +19 -0
  18. package/src/extension/bridge-dispatch.ts +41 -0
  19. package/src/extension/bridge-tool-call.ts +41 -0
  20. package/src/extension/host-facts.ts +18 -0
  21. package/src/extension/idle-parking-kernel.ts +272 -0
  22. package/src/extension/kernel-registration.ts +25 -0
  23. package/src/extension/kernel-tools-probe.ts +24 -0
  24. package/src/extension/removed-tool-hints.ts +15 -0
  25. package/src/extension/runtime-factory.ts +3 -0
  26. package/src/extension/session-manager-contract.ts +3 -0
  27. package/src/extension/session-manager.ts +58 -58
  28. package/src/extension/start-recovery.ts +111 -0
  29. package/src/handles/completion-handles.ts +202 -0
  30. package/src/handles/handle-args.ts +103 -0
  31. package/src/handles/handle-registry.ts +164 -0
  32. package/src/handles/wait.ts +214 -0
  33. package/src/handles/watch-queue.ts +102 -0
  34. package/src/index.ts +99 -94
  35. package/src/kernels/jl/handles.jl +124 -0
  36. package/src/kernels/jl/kernel.ts +3 -1
  37. package/src/kernels/jl/prelude.jl +9 -0
  38. package/src/kernels/jl/runner.jl +160 -1
  39. package/src/kernels/js/context-manager.ts +1 -1
  40. package/src/kernels/js/handles.js +93 -0
  41. package/src/kernels/js/kernel-tools-define.d.ts +6 -0
  42. package/src/kernels/js/kernel-tools-define.js +3 -1
  43. package/src/kernels/js/kernel-tools-registry.d.ts +2 -0
  44. package/src/kernels/js/kernel-tools-registry.js +18 -9
  45. package/src/kernels/js/kernel-tools-schema.js +6 -2
  46. package/src/kernels/js/kernel-tools-types.ts +2 -1
  47. package/src/kernels/js/worker-global-sizes.js +84 -14
  48. package/src/kernels/js/worker-runtime.js +10 -1
  49. package/src/kernels/py/host-cell.ts +42 -0
  50. package/src/kernels/py/kernel-contract.ts +2 -0
  51. package/src/kernels/py/kernel.ts +48 -4
  52. package/src/kernels/py/prelude.py +145 -4
  53. package/src/kernels/rb/handles.rb +97 -0
  54. package/src/kernels/rb/kernel.ts +3 -1
  55. package/src/kernels/rb/paths.rb +37 -0
  56. package/src/kernels/rb/prelude.rb +15 -38
  57. package/src/kernels/rb/runner.rb +141 -0
  58. package/src/kernels/shared/kernel-memory-host.ts +14 -6
  59. package/src/kernels/{js/kernel-tools-host.ts → shared/kernel-tools-pump.ts} +2 -2
  60. package/src/kernels/shared/process-group-cpu.ts +139 -0
  61. package/src/kernels/shared/subprocess-contract.ts +3 -0
  62. package/src/kernels/shared/subprocess-kernel.ts +33 -1
  63. package/src/kernels/shared/subprocess-startup.ts +90 -0
  64. package/src/tool/cell-handler.ts +21 -18
  65. package/src/tool/cell-reserved-dispatch.ts +51 -0
  66. package/src/tool/eval-tool-options.ts +3 -0
  67. package/src/tool/run-eval-cell.ts +7 -6
  68. package/src/tool/types.ts +15 -0
@@ -3,6 +3,9 @@ import { type Static, Type } from "typebox";
3
3
  import type { KernelMemoryThresholds } from "../bridge/memory-protocol.ts";
4
4
  import type { CodemodeSettings, Environment } from "./settings.ts";
5
5
 
6
+ // The longest delay setTimeout honours (2^31 - 1 ms); a longer one fires after 1 ms.
7
+ export const MAX_IDLE_PARK_MINUTES = 35_791;
8
+
6
9
  export const memorySettingsSchema = Type.Object(
7
10
  {
8
11
  gcWatermarkMb: Type.Optional(Type.Number({ minimum: 0 })),
@@ -10,6 +13,7 @@ export const memorySettingsSchema = Type.Object(
10
13
  ceilingMb: Type.Optional(Type.Number({ minimum: 0 })),
11
14
  retainedResultsMb: Type.Optional(Type.Number({ minimum: 0 })),
12
15
  retainedImagesMb: Type.Optional(Type.Number({ minimum: 0 })),
16
+ idleParkMinutes: Type.Optional(Type.Number({ minimum: 0, maximum: MAX_IDLE_PARK_MINUTES })),
13
17
  },
14
18
  { additionalProperties: false },
15
19
  );
@@ -27,6 +31,8 @@ export interface CodemodeMemorySettings {
27
31
  readonly retainedResultsMb: number;
28
32
  /** Disk budget (MiB) for settled-cell images spilled under the session artifacts dir; 0 keeps only the count cap. */
29
33
  readonly retainedImagesMb: number;
34
+ /** Minutes with no cell running or queued before a kernel is closed and restarted on the next cell; absent or 0 never parks. */
35
+ readonly idleParkMinutes?: number;
30
36
  }
31
37
 
32
38
  export const DEFAULT_MEMORY_GC_WATERMARK_MB = 256;
@@ -67,6 +73,7 @@ export function mergeMemorySettings(input: CodemodeMemorySettingsInput | undefin
67
73
  ceilingMb: input?.ceilingMb ?? defaults.ceilingMb,
68
74
  retainedResultsMb: input?.retainedResultsMb ?? defaults.retainedResultsMb,
69
75
  retainedImagesMb: input?.retainedImagesMb ?? defaults.retainedImagesMb,
76
+ ...(input?.idleParkMinutes === undefined ? {} : { idleParkMinutes: input.idleParkMinutes }),
70
77
  };
71
78
  }
72
79
 
@@ -91,6 +98,7 @@ export function validatedMemorySettings(input: CodemodeMemorySettingsInput | und
91
98
  ...defaults,
92
99
  retainedResultsMb: merged.retainedResultsMb,
93
100
  retainedImagesMb: merged.retainedImagesMb,
101
+ ...(merged.idleParkMinutes === undefined ? {} : { idleParkMinutes: merged.idleParkMinutes }),
94
102
  },
95
103
  warnings: [warning],
96
104
  };
@@ -0,0 +1,7 @@
1
+ export const HARD_LIMIT_ENVIRONMENT_FLAG = "SENPI_CODEMODE_HARD_LIMIT_SECONDS";
2
+ export const FOREGROUND_WINDOW_ENVIRONMENT_FLAG = "SENPI_CODEMODE_FOREGROUND_SECONDS";
3
+ export const RUN_BUDGET_ENVIRONMENT_FLAG = "SENPI_CODEMODE_RUN_BUDGET_SECONDS";
4
+ export const MAX_DETACHED_CELLS_ENVIRONMENT_FLAG = "SENPI_CODEMODE_MAX_DETACHED_CELLS";
5
+ export const DEFAULT_MAX_DETACHED_CELLS = 15;
6
+
7
+ export type Environment = Readonly<Record<string, string | undefined>>;
@@ -0,0 +1,72 @@
1
+ import type { CodemodeSettings } from "./settings.ts";
2
+ import {
3
+ DEFAULT_MAX_DETACHED_CELLS,
4
+ type Environment,
5
+ FOREGROUND_WINDOW_ENVIRONMENT_FLAG,
6
+ HARD_LIMIT_ENVIRONMENT_FLAG,
7
+ MAX_DETACHED_CELLS_ENVIRONMENT_FLAG,
8
+ RUN_BUDGET_ENVIRONMENT_FLAG,
9
+ } from "./settings-constants.ts";
10
+
11
+ const languageEnvironmentFlags = {
12
+ py: "SENPI_CODEMODE_PY",
13
+ js: "SENPI_CODEMODE_JS",
14
+ rb: "SENPI_CODEMODE_RB",
15
+ jl: "SENPI_CODEMODE_JL",
16
+ } as const;
17
+
18
+ export function resolveEnabledLanguages(
19
+ settings: CodemodeSettings,
20
+ env: Environment = process.env,
21
+ ): CodemodeSettings["languages"] {
22
+ return {
23
+ py: resolveLanguage(settings.languages.py, env[languageEnvironmentFlags.py]),
24
+ js: resolveLanguage(settings.languages.js, env[languageEnvironmentFlags.js]),
25
+ rb: resolveLanguage(settings.languages.rb, env[languageEnvironmentFlags.rb]),
26
+ jl: resolveLanguage(settings.languages.jl, env[languageEnvironmentFlags.jl]),
27
+ };
28
+ }
29
+
30
+ /** Environment override wins over the settings file; a non-positive or malformed value is ignored. */
31
+ export function resolveHardLimitSeconds(settings: CodemodeSettings, env: Environment = process.env): number {
32
+ return positiveSecondsOverride(env[HARD_LIMIT_ENVIRONMENT_FLAG]) ?? settings.hardLimitSeconds;
33
+ }
34
+
35
+ /** Environment override wins over the settings file; a non-positive or malformed value is ignored. */
36
+ export function resolveForegroundWindowSeconds(settings: CodemodeSettings, env: Environment = process.env): number {
37
+ return positiveSecondsOverride(env[FOREGROUND_WINDOW_ENVIRONMENT_FLAG]) ?? settings.foregroundWindowSeconds;
38
+ }
39
+
40
+ /** Environment override wins over the settings file; a non-positive or malformed value is ignored. */
41
+ export function resolveRunBudgetSeconds(settings: CodemodeSettings, env: Environment = process.env): number {
42
+ return positiveSecondsOverride(env[RUN_BUDGET_ENVIRONMENT_FLAG]) ?? settings.runBudgetSeconds;
43
+ }
44
+
45
+ /** Uses the same positive-integer environment parsing as the run budget. */
46
+ export function resolveMaxDetachedCells(settings: CodemodeSettings, env: Environment = process.env): number {
47
+ return (
48
+ positiveSecondsOverride(env[MAX_DETACHED_CELLS_ENVIRONMENT_FLAG]) ??
49
+ settings.maxDetachedCells ??
50
+ DEFAULT_MAX_DETACHED_CELLS
51
+ );
52
+ }
53
+
54
+ function positiveSecondsOverride(value: string | undefined): number | undefined {
55
+ if (value === undefined) return undefined;
56
+ const parsed = Number.parseInt(value, 10);
57
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
58
+ }
59
+
60
+ function resolveLanguage(fileSetting: boolean, environmentValue: string | undefined): boolean {
61
+ if (environmentValue === undefined) return fileSetting;
62
+ switch (environmentValue.trim().toLowerCase()) {
63
+ case "0":
64
+ case "false":
65
+ return false;
66
+ case "1":
67
+ case "true":
68
+ return true;
69
+ default:
70
+ return fileSetting;
71
+ }
72
+ }
@@ -4,7 +4,10 @@ import { join } from "node:path";
4
4
  import type { Static } from "typebox";
5
5
  import { Type } from "typebox";
6
6
  import { Check } from "typebox/value";
7
+ import { type CodemodeFeatureSettings, featureSettingsProperties, pickFeatureSettings } from "./feature-settings.ts";
7
8
  import { type CodemodeMemorySettings, memorySettingsSchema, validatedMemorySettings } from "./memory-settings.ts";
9
+ import { DEFAULT_MAX_DETACHED_CELLS } from "./settings-constants.ts";
10
+ import { withoutUnknownTopLevelKeys } from "./unknown-settings.ts";
8
11
 
9
12
  export const codemodeSettingsSchema = Type.Object(
10
13
  {
@@ -15,6 +18,7 @@ export const codemodeSettingsSchema = Type.Object(
15
18
  js: Type.Optional(Type.Boolean()),
16
19
  rb: Type.Optional(Type.Boolean()),
17
20
  jl: Type.Optional(Type.Boolean()),
21
+ pyInterpreter: Type.Optional(Type.String({ minLength: 1 })),
18
22
  },
19
23
  { additionalProperties: false },
20
24
  ),
@@ -45,6 +49,7 @@ export const codemodeSettingsSchema = Type.Object(
45
49
  ),
46
50
  statusEvents: Type.Optional(Type.Boolean()),
47
51
  memory: Type.Optional(memorySettingsSchema),
52
+ ...featureSettingsProperties,
48
53
  },
49
54
  { additionalProperties: false },
50
55
  );
@@ -61,12 +66,14 @@ export interface CodemodeOutputSink {
61
66
  readonly maxColumns: number;
62
67
  }
63
68
 
64
- export interface CodemodeSettings {
69
+ export interface CodemodeSettings extends CodemodeFeatureSettings {
65
70
  readonly languages: {
66
71
  readonly py: boolean;
67
72
  readonly js: boolean;
68
73
  readonly rb: boolean;
69
74
  readonly jl: boolean;
75
+ /** Explicit Python interpreter; unset keeps today's PATH detection. */
76
+ readonly pyInterpreter?: string;
70
77
  };
71
78
  /** Idle time an interactive call blocks the turn before the cell detaches; capped by the foreground window. */
72
79
  readonly cellTimeoutSeconds: number;
@@ -118,8 +125,6 @@ export interface LoadedCodemodeSettings {
118
125
  */
119
126
  export const DEFAULT_HARD_LIMIT_SECONDS = 1800;
120
127
 
121
- export const HARD_LIMIT_ENVIRONMENT_FLAG = "SENPI_CODEMODE_HARD_LIMIT_SECONDS";
122
-
123
128
  /**
124
129
  * Bash parity: `terminal/tools/foreground-window.ts` auto-detaches a still-running bash command to a
125
130
  * background session at 60s regardless of its `timeout` kill deadline. An eval cell gets the same
@@ -128,8 +133,6 @@ export const HARD_LIMIT_ENVIRONMENT_FLAG = "SENPI_CODEMODE_HARD_LIMIT_SECONDS";
128
133
  */
129
134
  export const DEFAULT_FOREGROUND_WINDOW_SECONDS = 60;
130
135
 
131
- export const FOREGROUND_WINDOW_ENVIRONMENT_FLAG = "SENPI_CODEMODE_FOREGROUND_SECONDS";
132
-
133
136
  /**
134
137
  * One language kernel runs one cell at a time and a killed JavaScript cell that cannot settle
135
138
  * cooperatively restarts its worker, so a runaway cell costs far more than a runaway bash command:
@@ -137,11 +140,6 @@ export const FOREGROUND_WINDOW_ENVIRONMENT_FLAG = "SENPI_CODEMODE_FOREGROUND_SEC
137
140
  */
138
141
  export const DEFAULT_RUN_BUDGET_SECONDS = 300;
139
142
 
140
- export const DEFAULT_MAX_DETACHED_CELLS = 15;
141
-
142
- export const RUN_BUDGET_ENVIRONMENT_FLAG = "SENPI_CODEMODE_RUN_BUDGET_SECONDS";
143
- export const MAX_DETACHED_CELLS_ENVIRONMENT_FLAG = "SENPI_CODEMODE_MAX_DETACHED_CELLS";
144
-
145
143
  // OMP settings-schema.ts:3211-3299 has language/path settings only; eval.ts:427
146
144
  // defaults timeout to 30s, and codemode pins concurrency-bridge.ts:30 width to 4.
147
145
  export const defaultCodemodeSettings: ResolvedCodemodeSettings = {
@@ -169,15 +167,6 @@ export const defaultCodemodeSettings: ResolvedCodemodeSettings = {
169
167
  memory: validatedMemorySettings(undefined).settings,
170
168
  };
171
169
 
172
- const languageEnvironmentFlags = {
173
- py: "SENPI_CODEMODE_PY",
174
- js: "SENPI_CODEMODE_JS",
175
- rb: "SENPI_CODEMODE_RB",
176
- jl: "SENPI_CODEMODE_JL",
177
- } as const;
178
-
179
- export type Environment = Readonly<Record<string, string | undefined>>;
180
-
181
170
  export async function loadCodemodeSettings(options: LoadCodemodeSettingsOptions = {}): Promise<LoadedCodemodeSettings> {
182
171
  const cwd = options.cwd ?? process.cwd();
183
172
  const homeDir = options.homeDir ?? homedir();
@@ -193,48 +182,6 @@ export async function loadCodemodeSettings(options: LoadCodemodeSettingsOptions
193
182
  return { settings: defaultCodemodeSettings, source: null, warnings: [] };
194
183
  }
195
184
 
196
- export function resolveEnabledLanguages(
197
- settings: CodemodeSettings,
198
- env: Environment = process.env,
199
- ): CodemodeSettings["languages"] {
200
- return {
201
- py: resolveLanguage(settings.languages.py, env[languageEnvironmentFlags.py]),
202
- js: resolveLanguage(settings.languages.js, env[languageEnvironmentFlags.js]),
203
- rb: resolveLanguage(settings.languages.rb, env[languageEnvironmentFlags.rb]),
204
- jl: resolveLanguage(settings.languages.jl, env[languageEnvironmentFlags.jl]),
205
- };
206
- }
207
-
208
- /** Environment override wins over the settings file; a non-positive or malformed value is ignored. */
209
- export function resolveHardLimitSeconds(settings: CodemodeSettings, env: Environment = process.env): number {
210
- return positiveSecondsOverride(env[HARD_LIMIT_ENVIRONMENT_FLAG]) ?? settings.hardLimitSeconds;
211
- }
212
-
213
- /** Environment override wins over the settings file; a non-positive or malformed value is ignored. */
214
- export function resolveForegroundWindowSeconds(settings: CodemodeSettings, env: Environment = process.env): number {
215
- return positiveSecondsOverride(env[FOREGROUND_WINDOW_ENVIRONMENT_FLAG]) ?? settings.foregroundWindowSeconds;
216
- }
217
-
218
- /** Environment override wins over the settings file; a non-positive or malformed value is ignored. */
219
- export function resolveRunBudgetSeconds(settings: CodemodeSettings, env: Environment = process.env): number {
220
- return positiveSecondsOverride(env[RUN_BUDGET_ENVIRONMENT_FLAG]) ?? settings.runBudgetSeconds;
221
- }
222
-
223
- /** Uses the same positive-integer environment parsing as the run budget. */
224
- export function resolveMaxDetachedCells(settings: CodemodeSettings, env: Environment = process.env): number {
225
- return (
226
- positiveSecondsOverride(env[MAX_DETACHED_CELLS_ENVIRONMENT_FLAG]) ??
227
- settings.maxDetachedCells ??
228
- DEFAULT_MAX_DETACHED_CELLS
229
- );
230
- }
231
-
232
- function positiveSecondsOverride(value: string | undefined): number | undefined {
233
- if (value === undefined) return undefined;
234
- const parsed = Number.parseInt(value, 10);
235
- return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
236
- }
237
-
238
185
  async function loadSettingsFile(path: string): Promise<LoadedCodemodeSettings> {
239
186
  const raw = await readFile(path, "utf8");
240
187
  let parsed: unknown;
@@ -249,15 +196,21 @@ async function loadSettingsFile(path: string): Promise<LoadedCodemodeSettings> {
249
196
  };
250
197
  }
251
198
 
252
- if (!Check(codemodeSettingsSchema, parsed)) {
199
+ const known = withoutUnknownTopLevelKeys(parsed, new Set(Object.keys(codemodeSettingsSchema.properties)), path);
200
+ const candidate = known.value;
201
+ if (!Check(codemodeSettingsSchema, candidate)) {
253
202
  return {
254
203
  settings: defaultCodemodeSettings,
255
204
  source: path,
256
- warnings: [`Invalid codemode settings in ${path}. Falling back to codemode defaults.`],
205
+ warnings: [...known.warnings, `Invalid codemode settings in ${path}. Falling back to codemode defaults.`],
257
206
  };
258
207
  }
259
208
 
260
- return { settings: mergeSettings(parsed), source: path, warnings: validatedMemorySettings(parsed.memory).warnings };
209
+ return {
210
+ settings: mergeSettings(candidate),
211
+ source: path,
212
+ warnings: [...known.warnings, ...validatedMemorySettings(candidate.memory).warnings],
213
+ };
261
214
  }
262
215
 
263
216
  function mergeSettings(input: CodemodeSettingsInput): ResolvedCodemodeSettings {
@@ -267,6 +220,7 @@ function mergeSettings(input: CodemodeSettingsInput): ResolvedCodemodeSettings {
267
220
  js: input.languages?.js ?? defaultCodemodeSettings.languages.js,
268
221
  rb: input.languages?.rb ?? defaultCodemodeSettings.languages.rb,
269
222
  jl: input.languages?.jl ?? defaultCodemodeSettings.languages.jl,
223
+ ...(input.languages?.pyInterpreter === undefined ? {} : { pyInterpreter: input.languages.pyInterpreter }),
270
224
  },
271
225
  cellTimeoutSeconds: input.cellTimeoutSeconds ?? defaultCodemodeSettings.cellTimeoutSeconds,
272
226
  foregroundWindowSeconds: input.foregroundWindowSeconds ?? defaultCodemodeSettings.foregroundWindowSeconds,
@@ -284,23 +238,10 @@ function mergeSettings(input: CodemodeSettingsInput): ResolvedCodemodeSettings {
284
238
  },
285
239
  statusEvents: input.statusEvents ?? defaultCodemodeSettings.statusEvents,
286
240
  memory: validatedMemorySettings(input.memory).settings,
241
+ ...pickFeatureSettings(input),
287
242
  };
288
243
  }
289
244
 
290
- function resolveLanguage(fileSetting: boolean, environmentValue: string | undefined): boolean {
291
- if (environmentValue === undefined) return fileSetting;
292
- switch (environmentValue.trim().toLowerCase()) {
293
- case "0":
294
- case "false":
295
- return false;
296
- case "1":
297
- case "true":
298
- return true;
299
- default:
300
- return fileSetting;
301
- }
302
- }
303
-
304
245
  async function fileExists(path: string): Promise<boolean> {
305
246
  try {
306
247
  await access(path);
@@ -309,3 +250,19 @@ async function fileExists(path: string): Promise<boolean> {
309
250
  return false;
310
251
  }
311
252
  }
253
+
254
+ export {
255
+ DEFAULT_MAX_DETACHED_CELLS,
256
+ type Environment,
257
+ FOREGROUND_WINDOW_ENVIRONMENT_FLAG,
258
+ HARD_LIMIT_ENVIRONMENT_FLAG,
259
+ MAX_DETACHED_CELLS_ENVIRONMENT_FLAG,
260
+ RUN_BUDGET_ENVIRONMENT_FLAG,
261
+ } from "./settings-constants.ts";
262
+ export {
263
+ resolveEnabledLanguages,
264
+ resolveForegroundWindowSeconds,
265
+ resolveHardLimitSeconds,
266
+ resolveMaxDetachedCells,
267
+ resolveRunBudgetSeconds,
268
+ } from "./settings-overrides.ts";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A settings file written for a newer senpi may name keys this version does not know: they are dropped with
3
+ * one warning each instead of failing the whole file, so the user's other settings still apply. Only the
4
+ * top level is lenient; the known nested objects keep their strict schemas.
5
+ */
6
+ export function withoutUnknownTopLevelKeys(
7
+ parsed: unknown,
8
+ known: ReadonlySet<string>,
9
+ path: string,
10
+ ): { readonly value: unknown; readonly warnings: string[] } {
11
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return { value: parsed, warnings: [] };
12
+ const kept: Record<string, unknown> = {};
13
+ const warnings: string[] = [];
14
+ for (const [key, value] of Object.entries(parsed)) {
15
+ if (known.has(key)) kept[key] = value;
16
+ else warnings.push(`Unknown codemode setting "${key}" in ${path} is ignored; the other settings still apply.`);
17
+ }
18
+ return { value: kept, warnings };
19
+ }
@@ -0,0 +1,41 @@
1
+ import type { ExtensionContext } from "@code-yeongyu/senpi";
2
+ import type { BridgeHttpCompletionRequest } from "../bridge/http-server.ts";
3
+ import { startCompletionHandle, wantsCompletionHandle } from "../completion/handle.ts";
4
+ import type { CompletionRequest } from "../completion/handler.ts";
5
+ import { resolveHardLimitSeconds } from "../config/settings.ts";
6
+ import type { CreateCodemodeSessionManagerOptions } from "./session-manager-contract.ts";
7
+
8
+ export interface BridgeDispatchContext {
9
+ /** The context of the cell that last reached the manager; the handle host and completion owner are read from it. */
10
+ readonly context: ExtensionContext | undefined;
11
+ readonly contextFor: (signal: AbortSignal) => ExtensionContext;
12
+ readonly requireContext: () => ExtensionContext;
13
+ }
14
+
15
+ /**
16
+ * `completion(prompt, handle=True)` from a subprocess kernel: the handle lives in the session registry and is
17
+ * bounded by the hard limit measured from this request, the tightest bound the bridge knows for its cell.
18
+ */
19
+ export async function dispatchBridgeCompletion(
20
+ options: CreateCodemodeSessionManagerOptions,
21
+ request: BridgeHttpCompletionRequest,
22
+ dispatch: BridgeDispatchContext,
23
+ ): Promise<unknown> {
24
+ const completion: CompletionRequest = { prompt: request.prompt, opts: request.opts };
25
+ if (!wantsCompletionHandle(request.opts)) {
26
+ return await options.complete(completion, dispatch.contextFor(request.signal));
27
+ }
28
+ const registry = options.handles;
29
+ if (registry === undefined) {
30
+ throw Object.assign(new Error("eval_wait_unavailable: completion handles are not available in this session"), {
31
+ code: "eval_wait_unavailable",
32
+ });
33
+ }
34
+ return startCompletionHandle({
35
+ registry,
36
+ request: completion,
37
+ complete: options.complete,
38
+ ctx: dispatch.requireContext(),
39
+ deadlineMs: Date.now() + resolveHardLimitSeconds(options.settings) * 1_000,
40
+ });
41
+ }
@@ -0,0 +1,41 @@
1
+ import type { EvalHandleHost } from "@code-yeongyu/senpi";
2
+ import { isReservedToolName, runReservedTool } from "../bridges/reserved-dispatch.ts";
3
+ import { defaultCodemodeSettings } from "../config/settings.ts";
4
+ import { marshalToolResult } from "../tool/image.ts";
5
+ import type { CreateCodemodeSessionManagerOptions } from "./session-manager-contract.ts";
6
+
7
+ export interface BridgeToolCallRequest {
8
+ readonly toolName: string;
9
+ readonly args: unknown;
10
+ readonly callId: string;
11
+ readonly signal: AbortSignal;
12
+ }
13
+
14
+ // Subprocess kernels (py/rb/jl) reach the host only through this route, so every reply
15
+ // must match the in-process JS path in tool/cell-handler.ts: reserved helper names dispatch
16
+ // through runReservedTool (forwarding them made agent() fail with "Unknown tool __agent__"),
17
+ // and ordinary tool results are marshalled to { text, images, details, hasError } — the raw
18
+ // { content } shape left python cells unable to reach tool.read image blocks.
19
+ export async function routeBridgeToolCall(
20
+ options: Pick<CreateCodemodeSessionManagerOptions, "executeTool" | "listTools" | "settings" | "handles">,
21
+ request: BridgeToolCallRequest,
22
+ evalHandleHost?: EvalHandleHost,
23
+ ): Promise<unknown> {
24
+ if (!isReservedToolName(request.toolName)) {
25
+ return marshalToolResult(await options.executeTool(request.toolName, request.args, { signal: request.signal }));
26
+ }
27
+ const taskTools = options.settings.taskTools ?? defaultCodemodeSettings.taskTools;
28
+ return await runReservedTool(request.toolName, {
29
+ callId: request.callId,
30
+ args: request.args,
31
+ executeTool: options.executeTool,
32
+ taskToolName: taskTools.task,
33
+ taskOutputToolName: taskTools.output,
34
+ listTools: options.listTools,
35
+ signal: request.signal,
36
+ emitStatus: () => {},
37
+ marshalToolResult,
38
+ ...(options.handles === undefined ? {} : { handles: options.handles }),
39
+ ...(evalHandleHost === undefined ? {} : { evalHandleHost }),
40
+ });
41
+ }
@@ -0,0 +1,18 @@
1
+ import * as os from "node:os";
2
+ import { jsRuntimeLabel } from "./runtime-info.ts";
3
+
4
+ /** The one-line host description the eval prompt shows (platform, CPU, cores, JS runtime). */
5
+ export function hostLine(): string {
6
+ const cpu = os.cpus()[0]?.model?.trim();
7
+ return [`${os.platform()} ${os.arch()}`, cpu, `${os.availableParallelism()} cores`, jsRuntimeLabel()]
8
+ .filter((part): part is string => !!part)
9
+ .join(" \u00b7 ");
10
+ }
11
+
12
+ /** The model id carried by a `model_select` event, when it has one. */
13
+ export function modelIdFrom(event: unknown): string | undefined {
14
+ if (typeof event !== "object" || event === null || !("model" in event)) return undefined;
15
+ const model = event.model;
16
+ if (typeof model !== "object" || model === null || !("id" in model)) return undefined;
17
+ return typeof model.id === "string" ? model.id : undefined;
18
+ }