@code-yeongyu/senpi 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.
Files changed (194) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/core/agent-session.d.ts +51 -5
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +168 -48
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/compaction/compaction.d.ts.map +1 -1
  7. package/dist/core/compaction/compaction.js +2 -1
  8. package/dist/core/compaction/compaction.js.map +1 -1
  9. package/dist/core/compaction/stuck-overflow.d.ts +11 -0
  10. package/dist/core/compaction/stuck-overflow.d.ts.map +1 -0
  11. package/dist/core/compaction/stuck-overflow.js +17 -0
  12. package/dist/core/compaction/stuck-overflow.js.map +1 -0
  13. package/dist/core/extensions/builtin/cache-keepalive/index.d.ts.map +1 -1
  14. package/dist/core/extensions/builtin/cache-keepalive/index.js +3 -21
  15. package/dist/core/extensions/builtin/cache-keepalive/index.js.map +1 -1
  16. package/dist/core/extensions/builtin/claude-sdk-oauth/session-binding.d.ts.map +1 -1
  17. package/dist/core/extensions/builtin/claude-sdk-oauth/session-binding.js +36 -19
  18. package/dist/core/extensions/builtin/claude-sdk-oauth/session-binding.js.map +1 -1
  19. package/dist/core/extensions/builtin/claude-sdk-oauth/session-commit-boundary.d.ts.map +1 -1
  20. package/dist/core/extensions/builtin/claude-sdk-oauth/session-commit-boundary.js +21 -10
  21. package/dist/core/extensions/builtin/claude-sdk-oauth/session-commit-boundary.js.map +1 -1
  22. package/dist/core/extensions/builtin/claude-sdk-oauth/session-continuity.d.ts +4 -17
  23. package/dist/core/extensions/builtin/claude-sdk-oauth/session-continuity.d.ts.map +1 -1
  24. package/dist/core/extensions/builtin/claude-sdk-oauth/session-continuity.js +23 -33
  25. package/dist/core/extensions/builtin/claude-sdk-oauth/session-continuity.js.map +1 -1
  26. package/dist/core/extensions/builtin/claude-sdk-oauth/session-stream.d.ts.map +1 -1
  27. package/dist/core/extensions/builtin/claude-sdk-oauth/session-stream.js +1 -0
  28. package/dist/core/extensions/builtin/claude-sdk-oauth/session-stream.js.map +1 -1
  29. package/dist/core/extensions/builtin/claude-sdk-oauth/session-sync.d.ts.map +1 -1
  30. package/dist/core/extensions/builtin/claude-sdk-oauth/session-sync.js +5 -3
  31. package/dist/core/extensions/builtin/claude-sdk-oauth/session-sync.js.map +1 -1
  32. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-attempt.d.ts.map +1 -1
  33. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-attempt.js +8 -3
  34. package/dist/core/extensions/builtin/claude-sdk-oauth/session-turn-attempt.js.map +1 -1
  35. package/dist/core/extensions/builtin/claude-sdk-oauth/tools.d.ts +6 -5
  36. package/dist/core/extensions/builtin/claude-sdk-oauth/tools.d.ts.map +1 -1
  37. package/dist/core/extensions/builtin/claude-sdk-oauth/tools.js +6 -5
  38. package/dist/core/extensions/builtin/claude-sdk-oauth/tools.js.map +1 -1
  39. package/dist/core/extensions/builtin/compaction/circuit-breaker.d.ts +8 -0
  40. package/dist/core/extensions/builtin/compaction/circuit-breaker.d.ts.map +1 -1
  41. package/dist/core/extensions/builtin/compaction/circuit-breaker.js +8 -0
  42. package/dist/core/extensions/builtin/compaction/circuit-breaker.js.map +1 -1
  43. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts.map +1 -1
  44. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +24 -1
  45. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js.map +1 -1
  46. package/dist/core/extensions/builtin/compaction/fallback-failed-turn-normalization.d.ts +18 -0
  47. package/dist/core/extensions/builtin/compaction/fallback-failed-turn-normalization.d.ts.map +1 -0
  48. package/dist/core/extensions/builtin/compaction/fallback-failed-turn-normalization.js +20 -0
  49. package/dist/core/extensions/builtin/compaction/fallback-failed-turn-normalization.js.map +1 -0
  50. package/dist/core/extensions/builtin/compaction/index.js +2 -2
  51. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  52. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +3 -0
  53. package/dist/core/extensions/builtin/compaction/lane-policy.d.ts.map +1 -1
  54. package/dist/core/extensions/builtin/compaction/lane-policy.js +47 -23
  55. package/dist/core/extensions/builtin/compaction/lane-policy.js.map +1 -1
  56. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +6 -4
  57. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js.map +1 -1
  58. package/dist/core/extensions/builtin/goal/cache-warm.d.ts +19 -1
  59. package/dist/core/extensions/builtin/goal/cache-warm.d.ts.map +1 -1
  60. package/dist/core/extensions/builtin/goal/cache-warm.js +23 -10
  61. package/dist/core/extensions/builtin/goal/cache-warm.js.map +1 -1
  62. package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts +1 -0
  63. package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts.map +1 -1
  64. package/dist/core/extensions/builtin/goal/continuation-recovery.js +2 -0
  65. package/dist/core/extensions/builtin/goal/continuation-recovery.js.map +1 -1
  66. package/dist/core/extensions/builtin/goal/continuation.d.ts +3 -1
  67. package/dist/core/extensions/builtin/goal/continuation.d.ts.map +1 -1
  68. package/dist/core/extensions/builtin/goal/continuation.js +4 -0
  69. package/dist/core/extensions/builtin/goal/continuation.js.map +1 -1
  70. package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts +3 -0
  71. package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts.map +1 -1
  72. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +14 -3
  73. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js.map +1 -1
  74. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts +1 -1
  75. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
  76. package/dist/core/extensions/builtin/goal/monitor-continuation.js +7 -3
  77. package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
  78. package/dist/core/extensions/builtin/hooks/plugin-manifest.js +2 -2
  79. package/dist/core/extensions/builtin/hooks/plugin-manifest.js.map +1 -1
  80. package/dist/core/extensions/builtin/hooks/safety.js +2 -2
  81. package/dist/core/extensions/builtin/hooks/safety.js.map +1 -1
  82. package/dist/core/extensions/builtin/permission-system/external-dir.d.ts.map +1 -1
  83. package/dist/core/extensions/builtin/permission-system/external-dir.js +5 -22
  84. package/dist/core/extensions/builtin/permission-system/external-dir.js.map +1 -1
  85. package/dist/core/extensions/builtin/permission-system/parsers.d.ts.map +1 -1
  86. package/dist/core/extensions/builtin/permission-system/parsers.js +5 -7
  87. package/dist/core/extensions/builtin/permission-system/parsers.js.map +1 -1
  88. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts.map +1 -1
  89. package/dist/core/extensions/builtin/terminal/monitor-registry.js +14 -7
  90. package/dist/core/extensions/builtin/terminal/monitor-registry.js.map +1 -1
  91. package/dist/core/package-identity.d.ts +12 -0
  92. package/dist/core/package-identity.d.ts.map +1 -0
  93. package/dist/core/package-identity.js +57 -0
  94. package/dist/core/package-identity.js.map +1 -0
  95. package/dist/core/resource-loader.d.ts +0 -2
  96. package/dist/core/resource-loader.d.ts.map +1 -1
  97. package/dist/core/resource-loader.js +8 -60
  98. package/dist/core/resource-loader.js.map +1 -1
  99. package/dist/core/tools/bounded-realpath.d.ts +28 -0
  100. package/dist/core/tools/bounded-realpath.d.ts.map +1 -0
  101. package/dist/core/tools/bounded-realpath.js +53 -0
  102. package/dist/core/tools/bounded-realpath.js.map +1 -0
  103. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  104. package/dist/core/tools/file-mutation-queue.js +15 -13
  105. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  106. package/dist/core/tools/filesystem-policy.d.ts +7 -0
  107. package/dist/core/tools/filesystem-policy.d.ts.map +1 -1
  108. package/dist/core/tools/filesystem-policy.js +25 -15
  109. package/dist/core/tools/filesystem-policy.js.map +1 -1
  110. package/dist/core/trust-manager.d.ts.map +1 -1
  111. package/dist/core/trust-manager.js +13 -2
  112. package/dist/core/trust-manager.js.map +1 -1
  113. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  114. package/dist/modes/interactive/interactive-mode.js +1 -0
  115. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  116. package/dist/modes/interactive/tips/catalog/memory-tips.d.ts.map +1 -1
  117. package/dist/modes/interactive/tips/catalog/memory-tips.js +6 -0
  118. package/dist/modes/interactive/tips/catalog/memory-tips.js.map +1 -1
  119. package/dist/modes/interactive/tips/catalog/subagent-tips.d.ts.map +1 -1
  120. package/dist/modes/interactive/tips/catalog/subagent-tips.js +6 -0
  121. package/dist/modes/interactive/tips/catalog/subagent-tips.js.map +1 -1
  122. package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
  123. package/dist/modes/rpc/host-lifecycle.js +31 -5
  124. package/dist/modes/rpc/host-lifecycle.js.map +1 -1
  125. package/dist/modes/rpc/host-watchdog.d.ts +11 -0
  126. package/dist/modes/rpc/host-watchdog.d.ts.map +1 -1
  127. package/dist/modes/rpc/host-watchdog.js +16 -2
  128. package/dist/modes/rpc/host-watchdog.js.map +1 -1
  129. package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
  130. package/dist/modes/rpc/multi-session-host.js +54 -16
  131. package/dist/modes/rpc/multi-session-host.js.map +1 -1
  132. package/dist/modes/rpc/socket-ownership.d.ts +46 -0
  133. package/dist/modes/rpc/socket-ownership.d.ts.map +1 -0
  134. package/dist/modes/rpc/socket-ownership.js +178 -0
  135. package/dist/modes/rpc/socket-ownership.js.map +1 -0
  136. package/dist/utils/fs-watch.d.ts +17 -0
  137. package/dist/utils/fs-watch.d.ts.map +1 -1
  138. package/dist/utils/fs-watch.js +38 -10
  139. package/dist/utils/fs-watch.js.map +1 -1
  140. package/dist/utils/paths.d.ts +33 -0
  141. package/dist/utils/paths.d.ts.map +1 -1
  142. package/dist/utils/paths.js +109 -3
  143. package/dist/utils/paths.js.map +1 -1
  144. package/docs/compaction-guide.md +2 -2
  145. package/docs/compaction.md +2 -2
  146. package/docs/development.md +3 -2
  147. package/docs/providers.md +1 -1
  148. package/docs/rpc.md +1 -1
  149. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +29 -0
  150. package/node_modules/@code-yeongyu/senpi-codemode/README.md +14 -1
  151. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  152. package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/protocol.ts +1 -0
  153. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/runtime-factory.ts +3 -0
  154. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/session-manager.ts +7 -0
  155. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/AGENTS.md +7 -0
  156. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/jl/kernel.ts +6 -2
  157. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/inline-worker.ts +2 -2
  158. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-contract.ts +3 -0
  159. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-core.js +29 -0
  160. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-indirect-eval.js +10 -6
  161. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-shell-capture.d.ts +16 -0
  162. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-shell-capture.js +44 -8
  163. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-startup.ts +3 -2
  164. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel-contract.ts +3 -0
  165. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/transport.ts +11 -3
  166. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/rb/kernel.ts +6 -2
  167. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/session-env.ts +56 -0
  168. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/runtime-asset.ts +40 -14
  169. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-contract.ts +3 -0
  170. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-kernel.ts +9 -1
  171. package/node_modules/@code-yeongyu/senpi-codemode/src/output/output-meta.ts +26 -2
  172. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/image.ts +12 -3
  173. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  174. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  175. package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
  176. package/node_modules/@earendil-works/pi-ai/dist/providers/data/azure-openai-responses.json +1 -1
  177. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  178. package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
  179. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai-codex.json +1 -1
  180. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json +1 -1
  181. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  182. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  183. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opengateway.json +1 -1
  184. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  185. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  186. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts +1 -1
  187. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts.map +1 -1
  188. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +14 -3
  189. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js.map +1 -1
  190. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  191. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  192. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  193. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  194. package/package.json +7 -7
@@ -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
  });
@@ -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
 
@@ -1,12 +1,15 @@
1
1
  import { join } from "node:path";
2
2
  import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
3
- import { type CodemodeRuntimeAssetEnvironment, resolveCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
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 resolveCodemodeRuntimeAsset(
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, resolveCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
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
- resolveCodemodeRuntimeAsset(localPath, join("kernels", "js", "inline-worker-entry.js"), options),
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 (/\breturn\b/u.test(persistentCode)) return `(async () => {\n${persistentCode}\n})()`;
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 = findLastTopLevelStatementStart(code);
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 findLastTopLevelStatementStart(code) {
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
- canStartRegex = REGEX_PREFIX_KEYWORDS.has(token);
767
- pendingControlParen = CONTROL_PAREN_KEYWORDS.has(token);
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
- function capturedSpawn(originalSpawn, options) {
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
- child = needsStderrCapture(spawnOptions)
114
- ? drainStderr(originalSpawn(first, { ...spawnOptions, stderr: "pipe" }), options.emitText)
115
- : originalSpawn(...args);
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
- child = needsStderrCapture(first)
118
- ? drainStderr(originalSpawn({ ...first, stderr: "pipe" }), options.emitText)
119
- : originalSpawn(...args);
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, resolveCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
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(resolveCodemodeRuntimeAsset(localPath, join("kernels", "js", "worker-entry.js"), options));
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 CodemodeRuntimeAssetEnvironment, resolveCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
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 resolveCodemodeRuntimeAsset(
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: { ...process.env, ...options.env, PYTHONUNBUFFERED: "1", PYTHONIOENCODING: "utf-8" },
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);
@@ -1,12 +1,15 @@
1
1
  import { join } from "node:path";
2
2
  import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/protocol.ts";
3
- import { type CodemodeRuntimeAssetEnvironment, resolveCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
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 resolveCodemodeRuntimeAsset(
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 = join(
19
- dirname(executablePath),
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, this.options);
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 }),