@code-yeongyu/senpi-codemode 2026.9.6 → 2026.9.7-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 +29 -0
- package/README.md +14 -1
- package/package.json +4 -4
- package/src/bridge/protocol.ts +1 -0
- package/src/extension/runtime-factory.ts +3 -0
- package/src/extension/session-manager.ts +7 -0
- package/src/kernels/AGENTS.md +7 -0
- package/src/kernels/jl/kernel.ts +6 -2
- package/src/kernels/js/inline-worker.ts +2 -2
- package/src/kernels/js/kernel-contract.ts +3 -0
- package/src/kernels/js/worker-core.js +29 -0
- package/src/kernels/js/worker-indirect-eval.js +10 -6
- package/src/kernels/js/worker-shell-capture.d.ts +16 -0
- package/src/kernels/js/worker-shell-capture.js +44 -8
- package/src/kernels/js/worker-startup.ts +3 -2
- package/src/kernels/py/kernel-contract.ts +3 -0
- package/src/kernels/py/transport.ts +11 -3
- package/src/kernels/rb/kernel.ts +6 -2
- package/src/kernels/session-env.ts +56 -0
- package/src/kernels/shared/runtime-asset.ts +40 -14
- package/src/kernels/shared/subprocess-contract.ts +3 -0
- package/src/kernels/shared/subprocess-kernel.ts +9 -1
- package/src/output/output-meta.ts +26 -2
- package/src/tool/image.ts +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,35 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.9.7-2] - 2026-09-07
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- The JS kernel's shell capture now pins the worker's environment view for `Bun.spawnSync` as well as `Bun.spawn`, so a cell calling it without an explicit `env` sees the session's `PI_*` values instead of the inherited OS environ.
|
|
26
|
+
- Eval kernels and every child they spawn now see the active session's `PI_*` environment (`PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, `PI_REASONING_LEVEL`) exactly as bash-tool children do: inherited `PI_*` values are dropped before the session values are applied, so subprocesses such as `omo-agent-toolkit ulw-loop` resolve the same session as the `bash` tool instead of a cwd-global one.
|
|
27
|
+
- JavaScript eval cells no longer lose their completion value when a nested function, callback, or try/catch helper contains `return`: the cell wrapper now skips last-expression capture only for a genuine top-level `return`, and a property named `return` no longer primes the statement scanner as the keyword (#1439).
|
|
28
|
+
- Eval output truncation notices now name the real cause: a width-clamped line reports `N line(s) clamped to M columns (… dropped)`, a byte-capped tail reports the actual cap, and a notice never presents the output's own size as a limit.
|
|
29
|
+
|
|
30
|
+
### Removed
|
|
31
|
+
|
|
32
|
+
## [2026.9.7] - 2026-09-07
|
|
33
|
+
|
|
34
|
+
### Breaking Changes
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
### Removed
|
|
43
|
+
|
|
15
44
|
## [2026.9.6] - 2026-09-06
|
|
16
45
|
|
|
17
46
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -54,6 +54,19 @@ task-tool names are known.
|
|
|
54
54
|
A missing optional interpreter removes that language from the session's `eval`
|
|
55
55
|
schema; it is not an installation failure.
|
|
56
56
|
|
|
57
|
+
### Session environment
|
|
58
|
+
|
|
59
|
+
Every kernel starts with the active session's `PI_*` environment — `PI_SESSION_ID`,
|
|
60
|
+
`PI_SESSION_FILE` (when the session is persistent), `PI_PROVIDER`, `PI_MODEL`, and
|
|
61
|
+
`PI_REASONING_LEVEL` (when set) — resolved at session start, mirroring the bash tool's
|
|
62
|
+
session environment contract. The values are visible to `env()`/`process.env`/`os.environ`
|
|
63
|
+
inside cells and are inherited by every child process a cell spawns
|
|
64
|
+
(`Bun.$`, `Bun.spawn`, `child_process`, `subprocess`, ...). Inherited `PI_*` values from
|
|
65
|
+
the launching environment are dropped first, so a child spawned from a cell sees exactly
|
|
66
|
+
what a child spawned from the bash tool sees. The values snapshot at kernel start, so a
|
|
67
|
+
mid-session model switch updates the bash tool's next command but not already-running
|
|
68
|
+
kernels; a new session starts fresh kernels with fresh values.
|
|
69
|
+
|
|
57
70
|
## Settings
|
|
58
71
|
|
|
59
72
|
Configuration is loaded in this order:
|
|
@@ -116,7 +129,7 @@ options object and asynchronous helpers are `await`-able.
|
|
|
116
129
|
| `print(value, ...)` | Emits text output. |
|
|
117
130
|
| `read(path, offset?, limit?)` | Reads text with 1-indexed line slicing. `local://` paths resolve under the session artifact root. |
|
|
118
131
|
| `write(path, content)` | Creates parent directories and writes text. `local://` paths persist in the session artifact root. |
|
|
119
|
-
| `env(key?, value?)` | Reads all kernel environment values, one value, or sets one value. |
|
|
132
|
+
| `env(key?, value?)` | Reads all kernel environment values, one value, or sets one value. Includes the session's `PI_*` values (see [Session environment](#session-environment)). |
|
|
120
133
|
| `tool.<name>(args)` | Invokes an active Senpi tool through the normal `pi.executeTool` pipeline and returns `{ text, images?, details?, hasError? }` in every kernel; image blocks arrive as `images[i] = { mimeType, dataBase64 }`. |
|
|
121
134
|
| `tool_schema(name?)` | Returns a tool's parameter schema without calling it; omit `name` to list tool names. |
|
|
122
135
|
| `completion(prompt, model?, system?, schema?)` | Requests a one-shot host completion; `schema` asks the host to parse structured output. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.7-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.9.
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.7-2",
|
|
34
34
|
"typebox": "1.3.18"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
37
|
+
"@code-yeongyu/senpi": "2026.9.7-2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.9.7-2"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
package/src/bridge/protocol.ts
CHANGED
|
@@ -26,6 +26,7 @@ const hostToKernelMessageSchema = Type.Union([
|
|
|
26
26
|
type: Type.Literal("init"),
|
|
27
27
|
sessionId: Type.String({ minLength: 1 }),
|
|
28
28
|
connection: connectionConfigSchema,
|
|
29
|
+
sessionEnv: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
29
30
|
}),
|
|
30
31
|
Type.Object({
|
|
31
32
|
type: Type.Literal("run"),
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getInterpreterAvailability,
|
|
14
14
|
type InterpreterAvailability,
|
|
15
15
|
} from "../interpreters/detect.ts";
|
|
16
|
+
import { sessionEnvironmentFrom } from "../kernels/session-env.ts";
|
|
16
17
|
import { resolveSessionArtifactsDir } from "../output/streaming-output.ts";
|
|
17
18
|
import type { EnabledEvalLanguages, EvalLanguage, EvalRuntimes } from "../tool/types.ts";
|
|
18
19
|
import { jsRuntimeInfo, runtimesFromAvailability } from "./runtime-info.ts";
|
|
@@ -66,11 +67,13 @@ export async function createRuntime(
|
|
|
66
67
|
const executeTool = createExecuteTool(pi, activeTools);
|
|
67
68
|
const create = options.createSessionManager ?? createCodemodeSessionManager;
|
|
68
69
|
const sessionId = sessionIdFrom(event);
|
|
70
|
+
const sessionEnv = sessionEnvironmentFrom(ctx);
|
|
69
71
|
const configuredPoolWidth = settings.parallelPoolWidth;
|
|
70
72
|
const parallelPoolWidth = Number.isFinite(configuredPoolWidth) ? Math.max(1, Math.trunc(configuredPoolWidth)) : 1;
|
|
71
73
|
const manager = await create({
|
|
72
74
|
sessionId,
|
|
73
75
|
cwd: ctx.cwd,
|
|
76
|
+
sessionEnv,
|
|
74
77
|
settings,
|
|
75
78
|
availability,
|
|
76
79
|
artifactsDir: artifacts.dir,
|
|
@@ -11,6 +11,7 @@ import { JuliaKernel } from "../kernels/jl/kernel.ts";
|
|
|
11
11
|
import { JavaScriptKernel } from "../kernels/js/context-manager.ts";
|
|
12
12
|
import { PythonKernel } from "../kernels/py/kernel.ts";
|
|
13
13
|
import { RubyKernel } from "../kernels/rb/kernel.ts";
|
|
14
|
+
import type { SessionEnvironment } from "../kernels/session-env.ts";
|
|
14
15
|
import { marshalToolResult } from "../tool/image.ts";
|
|
15
16
|
import type { EvalKernel, EvalKernelManager, EvalLanguage, ExecuteTool } from "../tool/types.ts";
|
|
16
17
|
|
|
@@ -40,6 +41,8 @@ export interface CreateCodemodeSessionManagerOptions {
|
|
|
40
41
|
readonly localRoots?: Readonly<Record<string, string>>;
|
|
41
42
|
/** Session-adjacent directory used for persisted eval artifacts. */
|
|
42
43
|
readonly artifactsDir?: string;
|
|
44
|
+
/** Per-session PI_* values exposed to every kernel and the children it spawns. */
|
|
45
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
43
46
|
readonly executeTool: ExecuteTool;
|
|
44
47
|
readonly listTools?: () => readonly EvalSchemaToolInfo[];
|
|
45
48
|
readonly complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
@@ -212,6 +215,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
212
215
|
cwd: this.#options.cwd,
|
|
213
216
|
parallelPoolWidth,
|
|
214
217
|
onMessage,
|
|
218
|
+
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
215
219
|
...(localRoots ? { localRoots: { ...localRoots } } : {}),
|
|
216
220
|
...(this.#options.artifactsDir ? { artifactsDir: this.#options.artifactsDir } : {}),
|
|
217
221
|
});
|
|
@@ -230,6 +234,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
230
234
|
interpreterPath: detected.path,
|
|
231
235
|
sessionId: this.#options.sessionId,
|
|
232
236
|
cwd: this.#options.cwd,
|
|
237
|
+
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
233
238
|
connection,
|
|
234
239
|
onMessage,
|
|
235
240
|
});
|
|
@@ -239,6 +244,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
239
244
|
command: detected.path,
|
|
240
245
|
sessionId: this.#options.sessionId,
|
|
241
246
|
cwd: this.#options.cwd,
|
|
247
|
+
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
242
248
|
connection,
|
|
243
249
|
onMessage,
|
|
244
250
|
});
|
|
@@ -247,6 +253,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
247
253
|
command: detected.path,
|
|
248
254
|
sessionId: this.#options.sessionId,
|
|
249
255
|
cwd: this.#options.cwd,
|
|
256
|
+
...(this.#options.sessionEnv ? { sessionEnv: this.#options.sessionEnv } : {}),
|
|
250
257
|
connection,
|
|
251
258
|
onMessage,
|
|
252
259
|
});
|
package/src/kernels/AGENTS.md
CHANGED
|
@@ -15,6 +15,7 @@ runner/prelude assets).
|
|
|
15
15
|
| Ruby kernel | `rb/kernel.ts` + `rb/prelude.rb`, `rb/runner.rb` |
|
|
16
16
|
| Julia kernel | `jl/kernel.ts` + `jl/prelude.jl`, `jl/runner.jl` |
|
|
17
17
|
| Shared subprocess layer | `shared/subprocess-kernel.ts`, `subprocess-{contract,process,queue,run}.ts`, `runtime-asset.ts` |
|
|
18
|
+
| Session environment | `session-env.ts` (PI_* contract shared by all kernels; mirrors the core bash tool) |
|
|
18
19
|
|
|
19
20
|
## CONVENTIONS
|
|
20
21
|
|
|
@@ -29,6 +30,12 @@ runner/prelude assets).
|
|
|
29
30
|
framed subprocesses through `shared/`.
|
|
30
31
|
- Subprocess retirement/restart, worker recovery, timeout, and interrupt
|
|
31
32
|
semantics live here, never in the tool layer.
|
|
33
|
+
- Every kernel exposes the active session's `PI_*` environment (`session-env.ts`):
|
|
34
|
+
inherited values are deleted before the session's values are applied, so any
|
|
35
|
+
child spawned from a cell sees the same session environment a bash-tool child
|
|
36
|
+
sees. The JS worker applies it at init (`worker-core.js`; shell capture pins
|
|
37
|
+
the env view under Bun because `delete process.env.X` does not unsetenv),
|
|
38
|
+
and py/rb/jl spawn with it merged into the interpreter environment.
|
|
32
39
|
|
|
33
40
|
## ANTI-PATTERNS
|
|
34
41
|
|
package/src/kernels/jl/kernel.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
|
-
import
|
|
3
|
+
import type { SessionEnvironment } from "../session-env.ts";
|
|
4
|
+
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
4
5
|
import { SubprocessKernel, type SubprocessSpawn } from "../shared/subprocess-kernel.ts";
|
|
5
6
|
|
|
6
7
|
export interface JuliaKernelStartOptions {
|
|
7
8
|
readonly cwd: string;
|
|
8
9
|
readonly sessionId: string;
|
|
9
10
|
readonly connection: BridgeConnectionConfig;
|
|
11
|
+
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
12
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
10
13
|
readonly command?: string;
|
|
11
14
|
readonly spawn?: SubprocessSpawn;
|
|
12
15
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
@@ -17,7 +20,7 @@ export interface JuliaRunnerPathOptions extends CodemodeRuntimeAssetEnvironment
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function resolveJuliaRunnerPath(options: JuliaRunnerPathOptions = {}): string {
|
|
20
|
-
return
|
|
23
|
+
return requireCodemodeRuntimeAsset(
|
|
21
24
|
options.localPath ?? join(import.meta.dirname, "runner.jl"),
|
|
22
25
|
join("kernels", "jl", "runner.jl"),
|
|
23
26
|
options,
|
|
@@ -42,6 +45,7 @@ export class JuliaKernel extends SubprocessKernel {
|
|
|
42
45
|
],
|
|
43
46
|
cwd: options.cwd,
|
|
44
47
|
sessionId: options.sessionId,
|
|
48
|
+
sessionEnv: options.sessionEnv,
|
|
45
49
|
connection: options.connection,
|
|
46
50
|
spawn: options.spawn,
|
|
47
51
|
onMessage: options.onMessage,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
3
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
4
|
-
import { type CodemodeRuntimeAssetEnvironment,
|
|
4
|
+
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
5
5
|
import type { JavaScriptKernelMode } from "./kernel-contract.ts";
|
|
6
6
|
import { spawnNodeWorker } from "./worker-host.ts";
|
|
7
7
|
|
|
@@ -12,7 +12,7 @@ export interface JavaScriptInlineWorkerEntryUrlOptions extends CodemodeRuntimeAs
|
|
|
12
12
|
export function resolveInlineWorkerEntryUrl(options: JavaScriptInlineWorkerEntryUrlOptions = {}): URL {
|
|
13
13
|
const localPath = options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "inline-worker-entry.js");
|
|
14
14
|
return pathToFileURL(
|
|
15
|
-
|
|
15
|
+
requireCodemodeRuntimeAsset(localPath, join("kernels", "js", "inline-worker-entry.js"), options),
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { SessionEnvironment } from "../session-env.ts";
|
|
2
3
|
|
|
3
4
|
export type ResultMessage = Extract<KernelToHostMessage, { type: "result" }>;
|
|
4
5
|
export type ToolCallMessage = Extract<KernelToHostMessage, { type: "tool-call" }>;
|
|
@@ -11,6 +12,8 @@ export interface JavaScriptKernelOptions {
|
|
|
11
12
|
readonly parallelPoolWidth: number;
|
|
12
13
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
13
14
|
readonly workerEntryUrl?: URL;
|
|
15
|
+
/** Per-session PI_* values applied to the worker environment before the first cell runs. */
|
|
16
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export interface JavaScriptRunInput {
|
|
@@ -3,6 +3,17 @@ import { JsWorkerRuntime } from "./worker-runtime.js";
|
|
|
3
3
|
// Mirrors INTERRUPT_ACK_OP in src/bridge/reserved.ts (this worker file cannot import TypeScript).
|
|
4
4
|
const INTERRUPT_ACK_OP = "interrupt-ack";
|
|
5
5
|
|
|
6
|
+
// Mirrors SESSION_ENVIRONMENT_KEYS in src/kernels/session-env.ts (this worker file
|
|
7
|
+
// cannot import TypeScript). Keys the active session does not set must be dropped so a
|
|
8
|
+
// value inherited from the host environment never leaks into a cell or its children.
|
|
9
|
+
const SESSION_ENVIRONMENT_KEYS = [
|
|
10
|
+
"PI_SESSION_ID",
|
|
11
|
+
"PI_SESSION_FILE",
|
|
12
|
+
"PI_PROVIDER",
|
|
13
|
+
"PI_MODEL",
|
|
14
|
+
"PI_REASONING_LEVEL",
|
|
15
|
+
];
|
|
16
|
+
|
|
6
17
|
export function createWorkerCore(transport, options) {
|
|
7
18
|
let runtime = null;
|
|
8
19
|
let activeCell = null;
|
|
@@ -54,6 +65,7 @@ export function createWorkerCore(transport, options) {
|
|
|
54
65
|
|
|
55
66
|
function onMessage(message) {
|
|
56
67
|
if (message.type === "init") {
|
|
68
|
+
applySessionEnvironment(message.sessionEnv);
|
|
57
69
|
runtime = new JsWorkerRuntime({
|
|
58
70
|
cwd: options.cwd,
|
|
59
71
|
parallelPoolWidth: options.parallelPoolWidth,
|
|
@@ -98,6 +110,23 @@ function durationMs(startedAtMs) {
|
|
|
98
110
|
return Math.max(0, Math.round(performance.now() - startedAtMs));
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
function applySessionEnvironment(sessionEnv) {
|
|
114
|
+
const provided = new Set(Object.keys(sessionEnv ?? {}));
|
|
115
|
+
const deleted = [];
|
|
116
|
+
for (const key of SESSION_ENVIRONMENT_KEYS) {
|
|
117
|
+
if (key in process.env && !provided.has(key)) deleted.push(key);
|
|
118
|
+
delete process.env[key];
|
|
119
|
+
}
|
|
120
|
+
const applied = Object.entries(sessionEnv ?? {});
|
|
121
|
+
for (const [key, value] of applied) process.env[key] = value;
|
|
122
|
+
// A worker's process.env is its own view: Bun.$ and node:child_process read it, but Bun.spawn
|
|
123
|
+
// without an explicit env inherits the OS environ, which also still holds deleted keys because
|
|
124
|
+
// `delete process.env.X` does not unsetenv under Bun. installShellCapture reads these flags and
|
|
125
|
+
// pins the worker's environment view for such children (see worker-shell-capture.js).
|
|
126
|
+
globalThis.__senpi_session_env_deletions__ = deleted;
|
|
127
|
+
globalThis.__senpi_session_env_applied__ = applied.length > 0 || deleted.length > 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
101
130
|
function valueRepr(value) {
|
|
102
131
|
if (value === undefined) return undefined;
|
|
103
132
|
return JSON.stringify(value);
|
|
@@ -11,7 +11,7 @@ export async function awaitMaybePromise(value) {
|
|
|
11
11
|
|
|
12
12
|
export function wrapUserCode(code) {
|
|
13
13
|
const persistentCode = persistTopLevelDeclarations(code);
|
|
14
|
-
if (
|
|
14
|
+
if (scanTopLevelStatements(persistentCode).hasTopLevelReturn) return `(async () => {\n${persistentCode}\n})()`;
|
|
15
15
|
return `(async () => {\n${captureLastExpression(persistentCode)}\n})()`;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -705,7 +705,7 @@ function skipBlockComment(code, start) {
|
|
|
705
705
|
}
|
|
706
706
|
|
|
707
707
|
function captureLastExpression(code) {
|
|
708
|
-
const start =
|
|
708
|
+
const start = scanTopLevelStatements(code).lastStatementStart;
|
|
709
709
|
const head = code.slice(0, start);
|
|
710
710
|
const tail = code.slice(start).trim();
|
|
711
711
|
if (!tail || isStatementOnly(tail)) return code;
|
|
@@ -726,7 +726,7 @@ function isStatementOnly(source) {
|
|
|
726
726
|
|
|
727
727
|
const CONTROL_PAREN_KEYWORDS = new Set(["catch", "for", "if", "switch", "while", "with"]);
|
|
728
728
|
|
|
729
|
-
function
|
|
729
|
+
function scanTopLevelStatements(code) {
|
|
730
730
|
let start = 0;
|
|
731
731
|
let round = 0;
|
|
732
732
|
let square = 0;
|
|
@@ -734,6 +734,7 @@ function findLastTopLevelStatementStart(code) {
|
|
|
734
734
|
let canStartRegex = true;
|
|
735
735
|
let lastSignificant = "";
|
|
736
736
|
let pendingControlParen = false;
|
|
737
|
+
let hasTopLevelReturn = false;
|
|
737
738
|
const controlParens = [];
|
|
738
739
|
for (let index = 0; index < code.length; index += 1) {
|
|
739
740
|
const char = code[index];
|
|
@@ -763,8 +764,11 @@ function findLastTopLevelStatementStart(code) {
|
|
|
763
764
|
if (isIdentifierStart(char)) {
|
|
764
765
|
const end = readIdentifier(code, index);
|
|
765
766
|
const token = code.slice(index, end);
|
|
766
|
-
|
|
767
|
-
|
|
767
|
+
const isPropertyName = lastSignificant === ".";
|
|
768
|
+
const atTopLevel = round === 0 && square === 0 && curly === 0;
|
|
769
|
+
if (token === "return" && atTopLevel && !isPropertyName) hasTopLevelReturn = true;
|
|
770
|
+
canStartRegex = !isPropertyName && REGEX_PREFIX_KEYWORDS.has(token);
|
|
771
|
+
pendingControlParen = !isPropertyName && CONTROL_PAREN_KEYWORDS.has(token);
|
|
768
772
|
lastSignificant = code[end - 1];
|
|
769
773
|
index = end - 1;
|
|
770
774
|
continue;
|
|
@@ -828,7 +832,7 @@ function findLastTopLevelStatementStart(code) {
|
|
|
828
832
|
pendingControlParen = false;
|
|
829
833
|
}
|
|
830
834
|
}
|
|
831
|
-
return start;
|
|
835
|
+
return { lastStatementStart: start, hasTopLevelReturn };
|
|
832
836
|
}
|
|
833
837
|
|
|
834
838
|
const STATEMENT_CONTINUATION_KEYWORDS = new Set(["catch", "else", "finally"]);
|
|
@@ -17,3 +17,19 @@ export interface ShellCaptureOptions {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function installShellCapture(options: ShellCaptureOptions): ShellCaptureRestore;
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
/**
|
|
23
|
+
* Set by the JS worker core when applying the session environment deleted inherited
|
|
24
|
+
* `PI_*` keys (see worker-core.js). Under Bun a `delete process.env.X` does not
|
|
25
|
+
* unsetenv, so shell capture pins the worker's environment view for spawned children
|
|
26
|
+
* while this list is non-empty.
|
|
27
|
+
*/
|
|
28
|
+
var __senpi_session_env_deletions__: string[] | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Set by the JS worker core once a session environment was applied (values set or inherited
|
|
31
|
+
* keys deleted). Bun.spawn without an explicit env inherits the OS environ rather than the
|
|
32
|
+
* worker's process.env, so shell capture pins the worker's view whenever this is true.
|
|
33
|
+
*/
|
|
34
|
+
var __senpi_session_env_applied__: boolean | undefined;
|
|
35
|
+
}
|
|
@@ -13,11 +13,23 @@ export function installShellCapture(options) {
|
|
|
13
13
|
if (!isBunRuntime(bun)) return () => {};
|
|
14
14
|
const originalShell = bun.$;
|
|
15
15
|
const originalSpawn = bun.spawn;
|
|
16
|
+
const originalSpawnSync = typeof bun.spawnSync === "function" ? bun.spawnSync : null;
|
|
17
|
+
const deletedKeys = globalThis.__senpi_session_env_deletions__;
|
|
18
|
+
const pinEnv =
|
|
19
|
+
globalThis.__senpi_session_env_applied__ === true || (Array.isArray(deletedKeys) && deletedKeys.length > 0);
|
|
20
|
+
if (pinEnv && typeof originalShell.env === "function") {
|
|
21
|
+
// Bun.spawn without an explicit env inherits the OS environ, not the worker's process.env,
|
|
22
|
+
// and deleting from process.env does not unsetenv under Bun. Pinning the worker's
|
|
23
|
+
// environment view mirrors the bash tool, which always spawns with an explicit env.
|
|
24
|
+
originalShell.env({ ...process.env });
|
|
25
|
+
}
|
|
16
26
|
bun.$ = capturedShell(originalShell, options);
|
|
17
|
-
bun.spawn = capturedSpawn(originalSpawn, options);
|
|
27
|
+
bun.spawn = capturedSpawn(originalSpawn, options, pinEnv);
|
|
28
|
+
if (originalSpawnSync !== null) bun.spawnSync = capturedSpawnSync(originalSpawnSync, pinEnv);
|
|
18
29
|
return () => {
|
|
19
30
|
bun.$ = originalShell;
|
|
20
31
|
bun.spawn = originalSpawn;
|
|
32
|
+
if (originalSpawnSync !== null) bun.spawnSync = originalSpawnSync;
|
|
21
33
|
};
|
|
22
34
|
}
|
|
23
35
|
|
|
@@ -103,20 +115,44 @@ function outputText(value) {
|
|
|
103
115
|
return typeof value === "string" ? value : "";
|
|
104
116
|
}
|
|
105
117
|
|
|
106
|
-
|
|
118
|
+
// Bun.spawnSync inherits the OS environ the same way Bun.spawn does, so a cell calling it
|
|
119
|
+
// without an explicit env must get the worker's view pinned too (measured on Bun 1.4.0).
|
|
120
|
+
function capturedSpawnSync(originalSpawnSync, pinEnv) {
|
|
121
|
+
return (...args) => {
|
|
122
|
+
if (!pinEnv) return originalSpawnSync(...args);
|
|
123
|
+
const [first, second] = args;
|
|
124
|
+
if (Array.isArray(first)) {
|
|
125
|
+
const spawnOptions = second === undefined ? {} : second;
|
|
126
|
+
if (spawnOptions === null || typeof spawnOptions !== "object" || spawnOptions.env !== undefined)
|
|
127
|
+
return originalSpawnSync(...args);
|
|
128
|
+
return originalSpawnSync(first, { ...spawnOptions, env: { ...process.env } });
|
|
129
|
+
}
|
|
130
|
+
if (first !== null && typeof first === "object" && first.env === undefined)
|
|
131
|
+
return originalSpawnSync({ ...first, env: { ...process.env } });
|
|
132
|
+
return originalSpawnSync(...args);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function capturedSpawn(originalSpawn, options, pinEnv) {
|
|
107
137
|
return (...args) => {
|
|
108
138
|
if (!options.isActive()) return originalSpawn(...args);
|
|
109
139
|
const [first, second] = args;
|
|
110
140
|
let child;
|
|
111
141
|
if (Array.isArray(first)) {
|
|
112
142
|
const spawnOptions = second === undefined ? {} : second;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
143
|
+
const effective = pinEnv && spawnOptions.env === undefined ? { ...spawnOptions, env: { ...process.env } } : spawnOptions;
|
|
144
|
+
child = needsStderrCapture(effective)
|
|
145
|
+
? drainStderr(originalSpawn(first, { ...effective, stderr: "pipe" }), options.emitText)
|
|
146
|
+
: effective === spawnOptions
|
|
147
|
+
? originalSpawn(...args)
|
|
148
|
+
: originalSpawn(first, effective);
|
|
116
149
|
} else {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
150
|
+
const effective = pinEnv && first !== null && typeof first === "object" && first.env === undefined ? { ...first, env: { ...process.env } } : first;
|
|
151
|
+
child = needsStderrCapture(effective)
|
|
152
|
+
? drainStderr(originalSpawn({ ...effective, stderr: "pipe" }), options.emitText)
|
|
153
|
+
: effective === first
|
|
154
|
+
? originalSpawn(...args)
|
|
155
|
+
: originalSpawn(effective);
|
|
120
156
|
}
|
|
121
157
|
options.onChild?.(child);
|
|
122
158
|
return child;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
-
import { type CodemodeRuntimeAssetEnvironment,
|
|
3
|
+
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
4
4
|
import { createInlineWorker, type WorkerLike } from "./inline-worker.ts";
|
|
5
5
|
import { type JavaScriptKernelOptions, localBridgeConnection } from "./local-module-loader.ts";
|
|
6
6
|
import { spawnNodeWorker, WorkerStartupCancelledError, waitForReady } from "./worker-host.ts";
|
|
@@ -11,7 +11,7 @@ export interface JavaScriptWorkerEntryUrlOptions extends CodemodeRuntimeAssetEnv
|
|
|
11
11
|
|
|
12
12
|
export function resolveJsWorkerEntryUrl(options: JavaScriptWorkerEntryUrlOptions = {}): URL {
|
|
13
13
|
const localPath = options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "worker-entry.js");
|
|
14
|
-
return pathToFileURL(
|
|
14
|
+
return pathToFileURL(requireCodemodeRuntimeAsset(localPath, join("kernels", "js", "worker-entry.js"), options));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface WorkerStartupHooks {
|
|
@@ -64,6 +64,7 @@ async function initializeWorker(
|
|
|
64
64
|
type: "init",
|
|
65
65
|
sessionId: options.sessionId,
|
|
66
66
|
connection: localBridgeConnection(options),
|
|
67
|
+
...(options.sessionEnv === undefined ? {} : { sessionEnv: options.sessionEnv }),
|
|
67
68
|
});
|
|
68
69
|
await ready;
|
|
69
70
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { SessionEnvironment } from "../session-env.ts";
|
|
2
3
|
import type { KernelSpawnProcess } from "./process.ts";
|
|
3
4
|
import type { PythonTransportResult } from "./transport.ts";
|
|
4
5
|
|
|
@@ -8,6 +9,8 @@ export interface PythonKernelStartOptions {
|
|
|
8
9
|
readonly cwd: string;
|
|
9
10
|
readonly connection: BridgeConnectionConfig;
|
|
10
11
|
readonly env?: NodeJS.ProcessEnv;
|
|
12
|
+
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
13
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
11
14
|
readonly startupTimeoutMs?: number;
|
|
12
15
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
13
16
|
readonly spawnProcess?: KernelSpawnProcess;
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
isKernelToHostMessage,
|
|
9
9
|
type KernelToHostMessage,
|
|
10
10
|
} from "../../bridge/protocol.ts";
|
|
11
|
-
import { type
|
|
11
|
+
import { applySessionEnvironment, type SessionEnvironment } from "../session-env.ts";
|
|
12
|
+
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
12
13
|
import {
|
|
13
14
|
defaultSpawn,
|
|
14
15
|
hardKill,
|
|
@@ -36,6 +37,8 @@ export interface PythonTransportOptions {
|
|
|
36
37
|
readonly cwd: string;
|
|
37
38
|
readonly connection: BridgeConnectionConfig;
|
|
38
39
|
readonly env?: NodeJS.ProcessEnv;
|
|
40
|
+
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
41
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
39
42
|
readonly startupTimeoutMs: number;
|
|
40
43
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
41
44
|
readonly spawnProcess?: KernelSpawnProcess;
|
|
@@ -53,7 +56,7 @@ export interface PythonPreludePathOptions extends CodemodeRuntimeAssetEnvironmen
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
export function resolvePythonPreludePath(options: PythonPreludePathOptions = {}): string {
|
|
56
|
-
return
|
|
59
|
+
return requireCodemodeRuntimeAsset(
|
|
57
60
|
options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "prelude.py"),
|
|
58
61
|
join("kernels", "py", "prelude.py"),
|
|
59
62
|
options,
|
|
@@ -83,7 +86,12 @@ export class PythonKernelTransport {
|
|
|
83
86
|
command: invocation.command,
|
|
84
87
|
args: [...invocation.args, "-u", scriptPath],
|
|
85
88
|
cwd: options.cwd,
|
|
86
|
-
env: {
|
|
89
|
+
env: {
|
|
90
|
+
...applySessionEnvironment(process.env, options.sessionEnv),
|
|
91
|
+
...options.env,
|
|
92
|
+
PYTHONUNBUFFERED: "1",
|
|
93
|
+
PYTHONIOENCODING: "utf-8",
|
|
94
|
+
},
|
|
87
95
|
};
|
|
88
96
|
const child = (options.spawnProcess ?? defaultSpawn)(spawnOptions);
|
|
89
97
|
const transport = new PythonKernelTransport(options, child);
|
package/src/kernels/rb/kernel.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
|
-
import
|
|
3
|
+
import type { SessionEnvironment } from "../session-env.ts";
|
|
4
|
+
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
4
5
|
import { SubprocessKernel, type SubprocessSpawn } from "../shared/subprocess-kernel.ts";
|
|
5
6
|
|
|
6
7
|
export interface RubyKernelStartOptions {
|
|
7
8
|
readonly cwd: string;
|
|
8
9
|
readonly sessionId: string;
|
|
9
10
|
readonly connection: BridgeConnectionConfig;
|
|
11
|
+
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
12
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
10
13
|
readonly command?: string;
|
|
11
14
|
readonly spawn?: SubprocessSpawn;
|
|
12
15
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
@@ -17,7 +20,7 @@ export interface RubyRunnerPathOptions extends CodemodeRuntimeAssetEnvironment {
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function resolveRubyRunnerPath(options: RubyRunnerPathOptions = {}): string {
|
|
20
|
-
return
|
|
23
|
+
return requireCodemodeRuntimeAsset(
|
|
21
24
|
options.localPath ?? join(import.meta.dirname, "runner.rb"),
|
|
22
25
|
join("kernels", "rb", "runner.rb"),
|
|
23
26
|
options,
|
|
@@ -31,6 +34,7 @@ export class RubyKernel extends SubprocessKernel {
|
|
|
31
34
|
args: [resolveRubyRunnerPath()],
|
|
32
35
|
cwd: options.cwd,
|
|
33
36
|
sessionId: options.sessionId,
|
|
37
|
+
sessionEnv: options.sessionEnv,
|
|
34
38
|
connection: options.connection,
|
|
35
39
|
spawn: options.spawn,
|
|
36
40
|
onMessage: options.onMessage,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session environment exposed to eval kernels and every child they spawn.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the shell-tool session environment contract in the senpi core
|
|
5
|
+
* (`resolveSpawnContext` in `packages/coding-agent/src/core/tools/bash.ts` and
|
|
6
|
+
* `docs/environment-variables.md`): the per-session `PI_*` variables are
|
|
7
|
+
* deleted from the inherited environment first, then set from the active
|
|
8
|
+
* session, so a stale inherited value never leaks into a kernel child. A child
|
|
9
|
+
* spawned from an eval cell must see the same session environment a child
|
|
10
|
+
* spawned from the bash tool sees.
|
|
11
|
+
*/
|
|
12
|
+
export const SESSION_ENVIRONMENT_KEYS = [
|
|
13
|
+
"PI_SESSION_ID",
|
|
14
|
+
"PI_SESSION_FILE",
|
|
15
|
+
"PI_PROVIDER",
|
|
16
|
+
"PI_MODEL",
|
|
17
|
+
"PI_REASONING_LEVEL",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
/** Resolved per-session values for {@link SESSION_ENVIRONMENT_KEYS}; absent keys stay unset. */
|
|
21
|
+
export type SessionEnvironment = Readonly<Record<string, string>>;
|
|
22
|
+
|
|
23
|
+
/** Structural slice of `ExtensionContext` the session environment is resolved from. */
|
|
24
|
+
export interface SessionEnvironmentSource {
|
|
25
|
+
readonly sessionManager: {
|
|
26
|
+
getSessionId(): string;
|
|
27
|
+
getSessionFile(): string | undefined;
|
|
28
|
+
};
|
|
29
|
+
readonly model?: { readonly provider: string; readonly id: string } | undefined;
|
|
30
|
+
readonly thinkingLevel?: string | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sessionEnvironmentFrom(source: SessionEnvironmentSource): SessionEnvironment {
|
|
34
|
+
const env: Record<string, string> = {};
|
|
35
|
+
env.PI_SESSION_ID = source.sessionManager.getSessionId();
|
|
36
|
+
const sessionFile = source.sessionManager.getSessionFile();
|
|
37
|
+
if (sessionFile) env.PI_SESSION_FILE = sessionFile;
|
|
38
|
+
const model = source.model;
|
|
39
|
+
if (model) {
|
|
40
|
+
env.PI_PROVIDER = model.provider;
|
|
41
|
+
env.PI_MODEL = model.id;
|
|
42
|
+
}
|
|
43
|
+
if (source.thinkingLevel) env.PI_REASONING_LEVEL = source.thinkingLevel;
|
|
44
|
+
return env;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Merges a session environment over a base environment the way the bash tool
|
|
49
|
+
* does: every {@link SESSION_ENVIRONMENT_KEYS} entry is dropped from `base`
|
|
50
|
+
* first, then the provided session values are applied.
|
|
51
|
+
*/
|
|
52
|
+
export function applySessionEnvironment(base: NodeJS.ProcessEnv, sessionEnv?: SessionEnvironment): NodeJS.ProcessEnv {
|
|
53
|
+
const merged: NodeJS.ProcessEnv = { ...base };
|
|
54
|
+
for (const key of SESSION_ENVIRONMENT_KEYS) delete merged[key];
|
|
55
|
+
return { ...merged, ...sessionEnv };
|
|
56
|
+
}
|
|
@@ -6,26 +6,52 @@ export interface CodemodeRuntimeAssetEnvironment {
|
|
|
6
6
|
readonly executablePath?: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export class CodemodeRuntimeAssetMissingError extends Error {
|
|
10
|
+
readonly packageRelativePath: string;
|
|
11
|
+
readonly localPath: string;
|
|
12
|
+
readonly sidecarPath: string;
|
|
13
|
+
readonly executablePath: string;
|
|
14
|
+
|
|
15
|
+
constructor(packageRelativePath: string, localPath: string, sidecarPath: string, executablePath: string) {
|
|
16
|
+
super(
|
|
17
|
+
`codemode runtime asset ${packageRelativePath} is unavailable: module-relative path ${localPath} is not readable and the codemode sidecar is missing beside the executable ${executablePath} (expected ${sidecarPath}). Ship node_modules/@code-yeongyu/senpi-codemode next to the executable.`,
|
|
18
|
+
);
|
|
19
|
+
this.name = "CodemodeRuntimeAssetMissingError";
|
|
20
|
+
this.packageRelativePath = packageRelativePath;
|
|
21
|
+
this.localPath = localPath;
|
|
22
|
+
this.sidecarPath = sidecarPath;
|
|
23
|
+
this.executablePath = executablePath;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isBunVirtualPath(path: string): boolean {
|
|
28
|
+
return path.includes("/$bunfs/") || path.includes("~BUN") || path.includes("%7EBUN");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sidecarPathFor(packageRelativePath: string, executablePath: string): string {
|
|
32
|
+
return join(dirname(executablePath), "node_modules", "@code-yeongyu", "senpi-codemode", "src", packageRelativePath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function requireCodemodeRuntimeAsset(
|
|
36
|
+
localPath: string,
|
|
37
|
+
packageRelativePath: string,
|
|
38
|
+
{ executablePath = process.execPath }: CodemodeRuntimeAssetEnvironment = {},
|
|
39
|
+
): string {
|
|
40
|
+
const sidecarPath = sidecarPathFor(packageRelativePath, executablePath);
|
|
41
|
+
if (!isBunVirtualPath(localPath) && existsSync(localPath)) return localPath;
|
|
42
|
+
if (existsSync(sidecarPath)) return sidecarPath;
|
|
43
|
+
throw new CodemodeRuntimeAssetMissingError(packageRelativePath, localPath, sidecarPath, executablePath);
|
|
44
|
+
}
|
|
45
|
+
|
|
9
46
|
export function resolveCodemodeRuntimeAsset(
|
|
10
47
|
localPath: string,
|
|
11
48
|
packageRelativePath: string,
|
|
12
49
|
{ bunVersion = process.versions.bun, executablePath = process.execPath }: CodemodeRuntimeAssetEnvironment = {},
|
|
13
50
|
): string {
|
|
14
|
-
if (existsSync(localPath))
|
|
15
|
-
return localPath;
|
|
16
|
-
}
|
|
51
|
+
if (existsSync(localPath)) return localPath;
|
|
17
52
|
if (bunVersion) {
|
|
18
|
-
const sidecarPath =
|
|
19
|
-
|
|
20
|
-
"node_modules",
|
|
21
|
-
"@code-yeongyu",
|
|
22
|
-
"senpi-codemode",
|
|
23
|
-
"src",
|
|
24
|
-
packageRelativePath,
|
|
25
|
-
);
|
|
26
|
-
if (existsSync(sidecarPath)) {
|
|
27
|
-
return sidecarPath;
|
|
28
|
-
}
|
|
53
|
+
const sidecarPath = sidecarPathFor(packageRelativePath, executablePath);
|
|
54
|
+
if (existsSync(sidecarPath)) return sidecarPath;
|
|
29
55
|
}
|
|
30
56
|
return localPath;
|
|
31
57
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { SessionEnvironment } from "../session-env.ts";
|
|
2
3
|
import type { SubprocessSpawn } from "./subprocess-process.ts";
|
|
3
4
|
|
|
4
5
|
export interface KernelRunInput {
|
|
@@ -15,6 +16,8 @@ export interface SubprocessKernelOptions {
|
|
|
15
16
|
readonly args: readonly string[];
|
|
16
17
|
readonly cwd?: string;
|
|
17
18
|
readonly env?: NodeJS.ProcessEnv;
|
|
19
|
+
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
20
|
+
readonly sessionEnv?: SessionEnvironment;
|
|
18
21
|
readonly sessionId: string;
|
|
19
22
|
readonly connection: BridgeConnectionConfig;
|
|
20
23
|
readonly spawn?: SubprocessSpawn;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
2
|
import { decodeBridgeFrame, encodeBridgeFrame, isKernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
3
|
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
4
|
+
import { applySessionEnvironment } from "../session-env.ts";
|
|
4
5
|
import type { KernelResult, KernelRunInput, SubprocessKernelOptions, ToolCallMessage } from "./subprocess-contract.ts";
|
|
5
6
|
import { type SubprocessLike, SubprocessProcess, type SubprocessSpawn, spawnSubprocess } from "./subprocess-process.ts";
|
|
6
7
|
import { SubprocessRunQueue } from "./subprocess-queue.ts";
|
|
@@ -133,7 +134,14 @@ export class SubprocessKernel {
|
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
private spawnProcess(): void {
|
|
136
|
-
const child = spawnSubprocess(this.options.spawn,
|
|
137
|
+
const child = spawnSubprocess(this.options.spawn, {
|
|
138
|
+
...this.options,
|
|
139
|
+
env:
|
|
140
|
+
this.options.env ??
|
|
141
|
+
(this.options.sessionEnv
|
|
142
|
+
? applySessionEnvironment(globalThis.process.env, this.options.sessionEnv)
|
|
143
|
+
: undefined),
|
|
144
|
+
});
|
|
137
145
|
const process = new SubprocessProcess(child, {
|
|
138
146
|
onLine: (source, line) => this.handleLine(source, line),
|
|
139
147
|
onStderr: (source, data) => this.handleMessage(source, { type: "text", stream: "stderr", data }),
|
|
@@ -28,12 +28,14 @@ export function resolveSessionArtifactsDir(sessionFile: string | undefined): Ses
|
|
|
28
28
|
|
|
29
29
|
export interface TruncationMeta {
|
|
30
30
|
readonly direction: "head" | "tail" | "middle";
|
|
31
|
-
readonly truncatedBy: "lines" | "bytes" | "middle";
|
|
31
|
+
readonly truncatedBy: "lines" | "bytes" | "columns" | "middle";
|
|
32
32
|
readonly totalLines: number;
|
|
33
33
|
readonly totalBytes: number;
|
|
34
34
|
readonly outputLines: number;
|
|
35
35
|
readonly outputBytes: number;
|
|
36
36
|
readonly maxBytes?: number;
|
|
37
|
+
readonly maxColumns?: number;
|
|
38
|
+
readonly columnTruncatedLines?: number;
|
|
37
39
|
readonly shownRange?: { readonly start: number; readonly end: number };
|
|
38
40
|
readonly headRange?: { readonly start: number; readonly end: number };
|
|
39
41
|
readonly tailRange?: { readonly start: number; readonly end: number };
|
|
@@ -72,7 +74,7 @@ export function formatTruncationWarning(meta: TruncationMeta | undefined): strin
|
|
|
72
74
|
meta.shownRange !== undefined && meta.shownRange.end >= meta.shownRange.start
|
|
73
75
|
? `Showing lines ${meta.shownRange.start}-${meta.shownRange.end} of ${meta.totalLines}`
|
|
74
76
|
: `Showing ${meta.outputLines} of ${meta.totalLines} lines`;
|
|
75
|
-
|
|
77
|
+
message += formatByteLoss(meta);
|
|
76
78
|
break;
|
|
77
79
|
default:
|
|
78
80
|
return assertNever(meta.direction);
|
|
@@ -81,6 +83,28 @@ export function formatTruncationWarning(meta: TruncationMeta | undefined): strin
|
|
|
81
83
|
return `[${message}]`;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
function formatByteLoss(meta: TruncationMeta): string {
|
|
87
|
+
const dropped = formatBytes(Math.max(0, meta.totalBytes - meta.outputBytes));
|
|
88
|
+
switch (meta.truncatedBy) {
|
|
89
|
+
case "columns": {
|
|
90
|
+
const clamped = meta.columnTruncatedLines ?? 0;
|
|
91
|
+
const width = meta.maxColumns === undefined ? "the column cap" : `${meta.maxColumns} columns`;
|
|
92
|
+
return `; ${clamped} line${clamped === 1 ? "" : "s"} clamped to ${width} (${dropped} dropped)`;
|
|
93
|
+
}
|
|
94
|
+
case "bytes":
|
|
95
|
+
return meta.maxBytes === undefined ? ` (${dropped} dropped)` : ` (${formatBytes(meta.maxBytes)} limit)`;
|
|
96
|
+
case "lines":
|
|
97
|
+
case "middle":
|
|
98
|
+
return "";
|
|
99
|
+
default:
|
|
100
|
+
return assertNeverCause(meta.truncatedBy);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertNeverCause(value: never): never {
|
|
105
|
+
throw new TypeError(`Unhandled truncation cause: ${String(value)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
export function stripOutputNotice(text: string, meta: TruncationMeta | undefined): string {
|
|
85
109
|
const notice = formatTruncationWarning(meta);
|
|
86
110
|
if (notice === null) return text;
|
package/src/tool/image.ts
CHANGED
|
@@ -119,7 +119,7 @@ export class EvalOutputCollector {
|
|
|
119
119
|
async finish(): Promise<EvalOutputResult> {
|
|
120
120
|
await this.#processImages();
|
|
121
121
|
const summary = await this.#finalSummary();
|
|
122
|
-
const meta = truncationMetaFromSummary(summary);
|
|
122
|
+
const meta = truncationMetaFromSummary(summary, this.#options.maxColumns);
|
|
123
123
|
const notice = summary.artifactId === undefined ? undefined : artifactNotice(summary.artifactId);
|
|
124
124
|
return {
|
|
125
125
|
output: summary.output.trimEnd(),
|
|
@@ -215,7 +215,7 @@ function formatDisplayJson(value: unknown): string {
|
|
|
215
215
|
return `${text.slice(0, MAX_DISPLAY_TEXT_BYTES)}\n[…${text.length - MAX_DISPLAY_TEXT_BYTES}ch elided…]`;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
function truncationMetaFromSummary(summary: OutputSummary): TruncationMeta | undefined {
|
|
218
|
+
function truncationMetaFromSummary(summary: OutputSummary, maxColumns: number): TruncationMeta | undefined {
|
|
219
219
|
if (!summary.truncated) return undefined;
|
|
220
220
|
const artifact = summary.artifactId === undefined ? {} : { artifactId: summary.artifactId };
|
|
221
221
|
if (summary.elidedBytes !== undefined && summary.elidedBytes > 0) {
|
|
@@ -239,9 +239,18 @@ function truncationMetaFromSummary(summary: OutputSummary): TruncationMeta | und
|
|
|
239
239
|
...artifact,
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
|
+
const droppedBytes = Math.max(0, summary.totalBytes - summary.outputBytes);
|
|
243
|
+
const clampedLines = summary.columnTruncatedLines ?? 0;
|
|
244
|
+
const columnOnly = clampedLines > 0 && (summary.columnDroppedBytes ?? 0) >= droppedBytes;
|
|
245
|
+
const byteCapped = summary.totalBytes - (summary.columnDroppedBytes ?? 0) > DEFAULT_MAX_BYTES;
|
|
242
246
|
return {
|
|
243
247
|
direction: "tail",
|
|
244
|
-
truncatedBy:
|
|
248
|
+
truncatedBy: columnOnly ? "columns" : byteCapped ? "bytes" : "lines",
|
|
249
|
+
...(columnOnly
|
|
250
|
+
? { maxColumns, columnTruncatedLines: clampedLines }
|
|
251
|
+
: byteCapped
|
|
252
|
+
? { maxBytes: DEFAULT_MAX_BYTES }
|
|
253
|
+
: {}),
|
|
245
254
|
totalLines: summary.totalLines,
|
|
246
255
|
totalBytes: summary.totalBytes,
|
|
247
256
|
outputLines: summary.outputLines,
|