@code-yeongyu/senpi-codemode 2026.10.5 → 2026.10.6
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 +22 -0
- package/README.md +16 -2
- package/package.json +4 -4
- package/src/bridge/kernel-tools-protocol.ts +1 -1
- package/src/config/feature-settings.ts +110 -0
- package/src/config/memory-settings.ts +8 -0
- package/src/config/settings-constants.ts +7 -0
- package/src/config/settings-overrides.ts +72 -0
- package/src/config/settings.ts +35 -78
- package/src/config/unknown-settings.ts +19 -0
- package/src/extension/bridge-tool-call.ts +37 -0
- package/src/extension/idle-parking-kernel.ts +272 -0
- package/src/extension/kernel-registration.ts +25 -0
- package/src/extension/kernel-tools-probe.ts +24 -0
- package/src/extension/session-manager.ts +42 -55
- package/src/kernels/jl/kernel.ts +3 -1
- package/src/kernels/jl/runner.jl +154 -0
- package/src/kernels/js/context-manager.ts +1 -1
- package/src/kernels/js/kernel-tools-types.ts +2 -1
- package/src/kernels/js/worker-global-sizes.js +84 -14
- package/src/kernels/py/prelude.py +6 -1
- package/src/kernels/rb/kernel.ts +3 -1
- package/src/kernels/rb/runner.rb +141 -0
- package/src/kernels/shared/kernel-memory-host.ts +14 -6
- package/src/kernels/{js/kernel-tools-host.ts → shared/kernel-tools-pump.ts} +2 -2
- package/src/kernels/shared/process-group-cpu.ts +139 -0
- package/src/kernels/shared/subprocess-contract.ts +3 -0
- package/src/kernels/shared/subprocess-kernel.ts +33 -1
- package/src/kernels/shared/subprocess-startup.ts +90 -0
- package/src/tool/run-eval-cell.ts +5 -6
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,28 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.10.6] - 2026-10-04
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- An opt-in `memory.idleParkMinutes` setting (off by default) closes a kernel that had no cell running or queued for that many minutes to give its memory back; the next cell starts a fresh kernel and its result says every earlier global is lost ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
22
|
+
- Ruby eval results name their largest globals, and Ruby and Julia kernels now get the same large-memory notice as JavaScript and Python when the interpreter footprint crosses `memory.noticeMb` ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
23
|
+
- Julia eval results name their largest globals in that notice ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Kernel tool descriptors may name any eval language (`js`, `py`, `rb`, `jl`), not only `js`; today only JavaScript kernels define tools, so nothing a session sees changes ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- A Ruby or Julia kernel whose start hangs no longer leaves its cells waiting forever: startup fails, naming the stage it stalled in, once the runner has printed nothing, changed no stage and its process group has used no CPU for 30 s, so a slow but busy start (a cold Julia compiling its prelude) is never cut off ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
30
|
+
- A `codemode.json` that names a key this version does not know no longer throws away every other setting: the unknown key gets one warning and the rest still apply (known nested objects stay strict). Optional keys for upcoming features (`environments`, `isolation`, `sandbox`, `prompt.advertiseHelpers`, `kernelTools`, `languages.pyInterpreter`) parse with today's behaviour as their defaults ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
31
|
+
|
|
32
|
+
- A Python cell's host calls (`tool.*`, `completion`) no longer go through a configured HTTP proxy: the loopback bridge request ignores proxy settings from the environment and, on Windows, the registry, so a proxy can't refuse a `127.0.0.1` call that never needed it ([#2619](https://github.com/code-yeongyu/senpi/issues/2619)).
|
|
33
|
+
- A JavaScript memory report no longer runs user code: array elements are read through their own descriptors (an index accessor is skipped and the estimate marked approximate), and typed arrays, buffers, Blob, Map and Set are sized through the built-in getters, so a subclass that overrides `byteLength` or `size` is never called ([#2452](https://github.com/code-yeongyu/senpi/issues/2452)).
|
|
34
|
+
|
|
35
|
+
### Removed
|
|
36
|
+
|
|
15
37
|
## [2026.10.5] - 2026-10-03
|
|
16
38
|
|
|
17
39
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -156,17 +156,31 @@ Configuration is loaded in this order:
|
|
|
156
156
|
| `outputSink.maxColumns` | `768` | Maximum columns per printed output line; `0` disables column clamping. A cell's return value is never column-clamped (the byte and line budgets still apply). |
|
|
157
157
|
| `statusEvents` | `true` | Enables kernel status-event forwarding and rendering. Each cell retains at most 100 status rows; after overflow, one omitted-count row precedes the latest 99 events. |
|
|
158
158
|
| `memory.gcWatermarkMb` | `256` | JavaScript kernel: a finished cell whose heap reached this size and grew runs a full collection before its result; whenever at least this much stays live, an idle full collection runs about a second after the cell, so dropped globals return their memory without a reset. Python kernel: a finished cell whose process footprint reached this size and grew runs `gc.collect()` (plus glibc `malloc_trim(0)` on Linux) before its result, and while at least this much stays live the next cells collect too (at most 1/20 of the time), so `del rows` returns its memory. `0` disables. Env override: `SENPI_CODEMODE_MEMORY_GC_WATERMARK_MB`. |
|
|
159
|
-
| `memory.noticeMb` | `1024` |
|
|
159
|
+
| `memory.noticeMb` | `1024` | When live memory (JS heap or Python footprint after a collection; Ruby and Julia interpreter footprint read by the host after each cell) reaches this size (first time, or 25% more than at the last notice), the result gets one bracketed notice naming the largest globals and how to drop them (`rows = undefined`, `del rows`, `rows = nil`, `rows = nothing`), plus `details.memory`. `0` disables. Env override: `SENPI_CODEMODE_MEMORY_NOTICE_MB`. |
|
|
160
160
|
| `memory.ceilingMb` | a quarter of physical memory, 2048-8192 | Every kernel (JS heap and Python footprint after a collection; Ruby and Julia interpreter footprint read by the host after each result, without a globals list): when live memory reaches this size, the result says so and the kernel restarts once no cell is running or queued on it; the next result says it was restarted (`details.memory.recycled`). `0` disables. Env override: `SENPI_CODEMODE_MEMORY_CEILING_MB`. Non-zero memory thresholds must satisfy watermark <= notice <= ceiling, otherwise all three use their defaults. |
|
|
161
161
|
| `memory.retainedResultsMb` | `32` | In-memory byte budget (MiB) for the settled cells kept for `peek`/`list`, on top of the 32-cell count cap; the oldest go first and the newest is always kept. `0` keeps only the count cap. Env override: `SENPI_CODEMODE_RETAINED_RESULTS_MB` (a non-negative integer). |
|
|
162
162
|
| `memory.retainedImagesMb` | `256` | Disk budget (MiB) for settled-cell images. Images of settled cells (foreground and detached) are written as base64 files under `<session artifacts>/settled-images/` instead of staying in memory, and `peek` reads them back, so it returns the full result. Beyond the budget the oldest files are deleted first; an evicted cell's files are deleted with it; the directory is removed when the session ends. A `peek` whose image file is gone returns the text plus a one-line note. `0` keeps only the count cap. Env override: `SENPI_CODEMODE_RETAINED_IMAGES_MB` (a non-negative integer). |
|
|
163
|
+
| `memory.idleParkMinutes` | `0` | Off by default. When greater than 0, a kernel with no cell running or queued for this many minutes is closed to give its memory back, and the next cell for that language starts a fresh one; that cell's result says the kernel was restarted and every earlier global is lost. Applies to every language. |
|
|
164
|
+
| `languages.pyInterpreter` | unset | Explicit Python interpreter path. Unset keeps today's `PATH` detection. |
|
|
165
|
+
| `environments.managedRoot` | unset | Root directory for managed per-session environments; unset uses the session's default location. |
|
|
166
|
+
| `environments.autoProvision` | `true` | Lets installs provision a managed environment. `false` makes installs refuse with `environment_installer_unavailable`. |
|
|
167
|
+
| `environments.js.installer` | `auto` | Installer for the managed JavaScript environment: `auto`, `bun` or `npm`. |
|
|
168
|
+
| `environments.py.installer` | `pip` | Installer for the managed Python environment. |
|
|
169
|
+
| `isolation.js` | `worker` | JavaScript kernel isolation: `worker` (today's worker thread) or `process`. Env override: `SENPI_CODEMODE_JS_ISOLATION`. |
|
|
170
|
+
| `sandbox.enabled` | `false` | Allows sandbox cells. While `false` the sandbox option is neither accepted nor advertised. |
|
|
171
|
+
| `sandbox.memoryMb` | `64` | Memory cap (MiB) for a sandbox cell. Env override: `SENPI_CODEMODE_SANDBOX_MEMORY_MB` (a positive integer wins). |
|
|
172
|
+
| `sandbox.timeoutSeconds` | `300` | Time limit for a sandbox cell. |
|
|
173
|
+
| `prompt.advertiseHelpers` | `false` | When `true`, one pointer line to `tool_schema('eval:helpers')` is appended to the eval description. |
|
|
174
|
+
| `kernelTools.enabled` | `true` | Allows cells to define kernel tools (`tool(fn)`, `@tool`). `false` makes them refuse with `tools_unavailable`. |
|
|
175
|
+
|
|
176
|
+
The `languages.pyInterpreter`, `environments.*`, `isolation.*`, `sandbox.*`, `prompt.*` and `kernelTools.*` keys are accepted and validated now, with the defaults shown, which match today's behaviour. The effect each of those rows describes takes effect when its feature ships; until then, setting a key changes nothing.
|
|
163
177
|
|
|
164
178
|
`SENPI_CODEMODE_PY`, `SENPI_CODEMODE_JS`, `SENPI_CODEMODE_RB`, and
|
|
165
179
|
`SENPI_CODEMODE_JL` override the corresponding file setting. `1` or `true`
|
|
166
180
|
enables; `0` or `false` disables. Any other value leaves the file setting in
|
|
167
181
|
effect.
|
|
168
182
|
|
|
169
|
-
Malformed JSON or invalid
|
|
183
|
+
A top-level key this version does not know is ignored with one warning naming it, and the rest of the file still applies, so a settings file written for a newer senpi keeps working. Malformed JSON, or an invalid value or an unknown key inside a known setting, falls back to all defaults with a warning.
|
|
170
184
|
The detached-cell environment override uses the run-budget parser: a positive
|
|
171
185
|
base-10 integer wins over the file value; zero, negative, and malformed values
|
|
172
186
|
leave the file value in effect.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.6",
|
|
4
4
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@babel/parser": "8.0.4",
|
|
35
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.
|
|
35
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.6",
|
|
36
36
|
"typebox": "1.3.34"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@code-yeongyu/senpi": "2026.10.
|
|
39
|
+
"@code-yeongyu/senpi": "2026.10.6"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@code-yeongyu/senpi": "2026.10.
|
|
42
|
+
"@code-yeongyu/senpi": "2026.10.6"
|
|
43
43
|
},
|
|
44
44
|
"keywords": [
|
|
45
45
|
"senpi",
|
|
@@ -29,7 +29,7 @@ export const kernelToolDescriptorSchema = Type.Object({
|
|
|
29
29
|
name: Type.String({ minLength: 1 }),
|
|
30
30
|
description: Type.String(),
|
|
31
31
|
input_schema: Type.Unknown(),
|
|
32
|
-
language: Type.Literal("js"),
|
|
32
|
+
language: Type.Union([Type.Literal("js"), Type.Literal("py"), Type.Literal("rb"), Type.Literal("jl")]),
|
|
33
33
|
kernel_generation: Type.Integer({ minimum: 0 }),
|
|
34
34
|
definition_revision: Type.Integer({ minimum: 1 }),
|
|
35
35
|
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Static } from "typebox";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
5
|
+
|
|
6
|
+
export const JS_ISOLATION_ENVIRONMENT_FLAG = "SENPI_CODEMODE_JS_ISOLATION";
|
|
7
|
+
export const SANDBOX_MEMORY_ENVIRONMENT_FLAG = "SENPI_CODEMODE_SANDBOX_MEMORY_MB";
|
|
8
|
+
|
|
9
|
+
export const featureSettingsProperties = {
|
|
10
|
+
environments: Type.Optional(
|
|
11
|
+
Type.Object(
|
|
12
|
+
{
|
|
13
|
+
managedRoot: Type.Optional(Type.String({ minLength: 1 })),
|
|
14
|
+
autoProvision: Type.Optional(Type.Boolean()),
|
|
15
|
+
js: Type.Optional(
|
|
16
|
+
Type.Object(
|
|
17
|
+
{ installer: Type.Union([Type.Literal("auto"), Type.Literal("bun"), Type.Literal("npm")]) },
|
|
18
|
+
{ additionalProperties: false },
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
py: Type.Optional(Type.Object({ installer: Type.Literal("pip") }, { additionalProperties: false })),
|
|
22
|
+
},
|
|
23
|
+
{ additionalProperties: false },
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
isolation: Type.Optional(
|
|
27
|
+
Type.Object(
|
|
28
|
+
{ js: Type.Optional(Type.Union([Type.Literal("worker"), Type.Literal("process")])) },
|
|
29
|
+
{ additionalProperties: false },
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
sandbox: Type.Optional(
|
|
33
|
+
Type.Object(
|
|
34
|
+
{
|
|
35
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
36
|
+
memoryMb: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
37
|
+
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
38
|
+
},
|
|
39
|
+
{ additionalProperties: false },
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
prompt: Type.Optional(
|
|
43
|
+
Type.Object({ advertiseHelpers: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
44
|
+
),
|
|
45
|
+
kernelTools: Type.Optional(Type.Object({ enabled: Type.Optional(Type.Boolean()) }, { additionalProperties: false })),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const featureSettingsSchema = Type.Object(featureSettingsProperties);
|
|
49
|
+
|
|
50
|
+
export type CodemodeFeatureSettings = Static<typeof featureSettingsSchema>;
|
|
51
|
+
|
|
52
|
+
export type JsIsolation = "worker" | "process";
|
|
53
|
+
|
|
54
|
+
export interface ResolvedSandbox {
|
|
55
|
+
readonly enabled: boolean;
|
|
56
|
+
readonly memoryMb: number;
|
|
57
|
+
readonly timeoutSeconds: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ResolvedEnvironments {
|
|
61
|
+
readonly managedRoot?: string;
|
|
62
|
+
readonly autoProvision: boolean;
|
|
63
|
+
readonly jsInstaller: "auto" | "bun" | "npm";
|
|
64
|
+
readonly pyInstaller: "pip";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function pickFeatureSettings(input: CodemodeFeatureSettings): CodemodeFeatureSettings {
|
|
68
|
+
return {
|
|
69
|
+
...(input.environments === undefined ? {} : { environments: input.environments }),
|
|
70
|
+
...(input.isolation === undefined ? {} : { isolation: input.isolation }),
|
|
71
|
+
...(input.sandbox === undefined ? {} : { sandbox: input.sandbox }),
|
|
72
|
+
...(input.prompt === undefined ? {} : { prompt: input.prompt }),
|
|
73
|
+
...(input.kernelTools === undefined ? {} : { kernelTools: input.kernelTools }),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The environment wins over the file; an unknown value is ignored. Default: today's worker-thread kernel. */
|
|
78
|
+
export function resolveJsIsolation(settings: CodemodeFeatureSettings, env: Environment = process.env): JsIsolation {
|
|
79
|
+
const fromEnv = env[JS_ISOLATION_ENVIRONMENT_FLAG];
|
|
80
|
+
if (fromEnv === "worker" || fromEnv === "process") return fromEnv;
|
|
81
|
+
return settings.isolation?.js ?? "worker";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Off by default; the environment's memory cap wins over the file when it is a positive integer. */
|
|
85
|
+
export function resolveSandbox(settings: CodemodeFeatureSettings, env: Environment = process.env): ResolvedSandbox {
|
|
86
|
+
const fromEnv = Number.parseInt(env[SANDBOX_MEMORY_ENVIRONMENT_FLAG] ?? "", 10);
|
|
87
|
+
return {
|
|
88
|
+
enabled: settings.sandbox?.enabled ?? false,
|
|
89
|
+
memoryMb: Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : (settings.sandbox?.memoryMb ?? 64),
|
|
90
|
+
timeoutSeconds: settings.sandbox?.timeoutSeconds ?? 300,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveEnvironments(settings: CodemodeFeatureSettings): ResolvedEnvironments {
|
|
95
|
+
const environments = settings.environments;
|
|
96
|
+
return {
|
|
97
|
+
...(environments?.managedRoot === undefined ? {} : { managedRoot: environments.managedRoot }),
|
|
98
|
+
autoProvision: environments?.autoProvision ?? true,
|
|
99
|
+
jsInstaller: environments?.js?.installer ?? "auto",
|
|
100
|
+
pyInstaller: environments?.py?.installer ?? "pip",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function resolveAdvertiseHelpers(settings: CodemodeFeatureSettings): boolean {
|
|
105
|
+
return settings.prompt?.advertiseHelpers ?? false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function resolveKernelToolsEnabled(settings: CodemodeFeatureSettings): boolean {
|
|
109
|
+
return settings.kernelTools?.enabled ?? true;
|
|
110
|
+
}
|
|
@@ -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
|
+
}
|
package/src/config/settings.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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,37 @@
|
|
|
1
|
+
import { isReservedToolName, runReservedTool } from "../bridges/reserved-dispatch.ts";
|
|
2
|
+
import { defaultCodemodeSettings } from "../config/settings.ts";
|
|
3
|
+
import { marshalToolResult } from "../tool/image.ts";
|
|
4
|
+
import type { CreateCodemodeSessionManagerOptions } from "./session-manager-contract.ts";
|
|
5
|
+
|
|
6
|
+
export interface BridgeToolCallRequest {
|
|
7
|
+
readonly toolName: string;
|
|
8
|
+
readonly args: unknown;
|
|
9
|
+
readonly callId: string;
|
|
10
|
+
readonly signal: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Subprocess kernels (py/rb/jl) reach the host only through this route, so every reply
|
|
14
|
+
// must match the in-process JS path in tool/cell-handler.ts: reserved helper names dispatch
|
|
15
|
+
// through runReservedTool (forwarding them made agent() fail with "Unknown tool __agent__"),
|
|
16
|
+
// and ordinary tool results are marshalled to { text, images, details, hasError } — the raw
|
|
17
|
+
// { content } shape left python cells unable to reach tool.read image blocks.
|
|
18
|
+
export async function routeBridgeToolCall(
|
|
19
|
+
options: Pick<CreateCodemodeSessionManagerOptions, "executeTool" | "listTools" | "settings">,
|
|
20
|
+
request: BridgeToolCallRequest,
|
|
21
|
+
): Promise<unknown> {
|
|
22
|
+
if (!isReservedToolName(request.toolName)) {
|
|
23
|
+
return marshalToolResult(await options.executeTool(request.toolName, request.args, { signal: request.signal }));
|
|
24
|
+
}
|
|
25
|
+
const taskTools = options.settings.taskTools ?? defaultCodemodeSettings.taskTools;
|
|
26
|
+
return await runReservedTool(request.toolName, {
|
|
27
|
+
callId: request.callId,
|
|
28
|
+
args: request.args,
|
|
29
|
+
executeTool: options.executeTool,
|
|
30
|
+
taskToolName: taskTools.task,
|
|
31
|
+
taskOutputToolName: taskTools.output,
|
|
32
|
+
listTools: options.listTools,
|
|
33
|
+
signal: request.signal,
|
|
34
|
+
emitStatus: () => {},
|
|
35
|
+
marshalToolResult,
|
|
36
|
+
});
|
|
37
|
+
}
|