@code-yeongyu/senpi-codemode 2026.9.12 → 2026.9.13-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,6 +12,58 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.9.13-2] - 2026-09-13
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ - Added `PI_SESSION_CWD` and `PI_GOAL_STORE_FILE` session environment keys for kernels and shell children, including clearing of inherited values when absent (fixes #1663).
22
+
23
+ - Interactive foreground eval cells detach on queued steering without cancelling their computation or in-flight tools. An occupied detached slot keeps the call waiting ([#1637](https://github.com/code-yeongyu/senpi/issues/1637)).
24
+
25
+ ### Changed
26
+
27
+ ### Fixed
28
+
29
+ ### Removed
30
+
31
+ ## [2026.9.13] - 2026-09-13
32
+
33
+ ### Breaking Changes
34
+
35
+ ### Added
36
+
37
+ ### Changed
38
+
39
+ ### Fixed
40
+
41
+ ### Removed
42
+
43
+ ## [2026.9.12-3] - 2026-09-12
44
+
45
+ ### Breaking Changes
46
+
47
+ ### Added
48
+
49
+ ### Changed
50
+
51
+ ### Fixed
52
+
53
+ ### Removed
54
+
55
+ ## [2026.9.12-2] - 2026-09-12
56
+
57
+ ### Breaking Changes
58
+
59
+ ### Added
60
+
61
+ ### Changed
62
+
63
+ ### Fixed
64
+
65
+ ### Removed
66
+
15
67
  ## [2026.9.12] - 2026-09-12
16
68
 
17
69
  ### Breaking Changes
package/README.md CHANGED
@@ -57,8 +57,9 @@ schema; it is not an installation failure.
57
57
  ### Session environment
58
58
 
59
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
60
+ `PI_SESSION_FILE` (when the session is persistent), `PI_SESSION_CWD` (the session's
61
+ working directory), `PI_GOAL_STORE_FILE` (the authoritative goal-store path, when the
62
+ host provides it), `PI_PROVIDER`, `PI_MODEL`, and `PI_REASONING_LEVEL` (when set) — resolved at session start, mirroring the bash tool's
62
63
  session environment contract. The values are visible to `env()`/`process.env`/`os.environ`
63
64
  inside cells and are inherited by every child process a cell spawns
64
65
  (`Bun.$`, `Bun.spawn`, `child_process`, `subprocess`, ...). Inherited `PI_*` values from
@@ -67,6 +68,11 @@ what a child spawned from the bash tool sees. The values snapshot at kernel star
67
68
  mid-session model switch updates the bash tool's next command but not already-running
68
69
  kernels; a new session starts fresh kernels with fresh values.
69
70
 
71
+ `PI_GOAL_STORE_FILE` is supplied by the host's optional `ExtensionContext.goalStoreFile`
72
+ getter and may name a file that does not exist yet. It honors session-directory overrides
73
+ and in-memory sessions; it cannot be derived reliably from `PI_SESSION_FILE`. If the host
74
+ omits the getter, the variable is unset rather than inherited from the launching process.
75
+
70
76
  ## Settings
71
77
 
72
78
  Configuration is loaded in this order:
@@ -177,6 +183,12 @@ cell keeps only its own language kernel busy. A new same-language call returns
177
183
  a busy error with its cell id and output tail; calls in other languages continue
178
184
  normally. Do not re-run the cell.
179
185
 
186
+ Queued steering also detaches an eligible interactive foreground call, including
187
+ one paused in a host tool bridge, without cancelling its computation. If the
188
+ language's detached slot is occupied, steering leaves the call waiting. Follow-up
189
+ messages, explicit `on_timeout: "error"`, and print/JSON calls do not trigger this
190
+ transition; caller abort and existing deadlines retain their cancellation behavior.
191
+
180
192
  Every cell, detached or not, is bounded by two kill deadlines. The run budget
181
193
  (`runBudgetSeconds`, or the call's `timeout`) charges only the cell's own
182
194
  execution time and is paused while a host tool call is in flight, so a cell
@@ -185,8 +197,8 @@ The hard limit (`hardLimitSeconds`, raised by a larger `timeout`) is wall-clock
185
197
  and bounds parked cells too. A cell killed by either deadline reports which one
186
198
  in its result or completion notification, together with whether kernel state
187
199
  survived; the tool schema states the configured numbers. The `timeout` value
188
- never changes when an interactive call detaches: that is `cellTimeoutSeconds`
189
- capped by `foregroundWindowSeconds`.
200
+ never changes the idle detach deadline: that is `cellTimeoutSeconds` capped by
201
+ `foregroundWindowSeconds`; queued steering can detach the call earlier.
190
202
 
191
203
  While any cell is detached, the interactive footer shows a highlighted
192
204
  `↗ <language> · <summary>` status on the extension status line (the cell id
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.9.12",
3
+ "version": "2026.9.13-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.12",
34
- "typebox": "1.3.18"
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.13-2",
34
+ "typebox": "1.3.27"
35
35
  },
36
36
  "peerDependencies": {
37
- "@code-yeongyu/senpi": "2026.9.12"
37
+ "@code-yeongyu/senpi": "2026.9.13-2"
38
38
  },
39
39
  "devDependencies": {
40
- "@code-yeongyu/senpi": "2026.9.12"
40
+ "@code-yeongyu/senpi": "2026.9.13-2"
41
41
  },
42
42
  "keywords": [
43
43
  "senpi",
@@ -9,6 +9,8 @@ const INTERRUPT_ACK_OP = "interrupt-ack";
9
9
  const SESSION_ENVIRONMENT_KEYS = [
10
10
  "PI_SESSION_ID",
11
11
  "PI_SESSION_FILE",
12
+ "PI_SESSION_CWD",
13
+ "PI_GOAL_STORE_FILE",
12
14
  "PI_PROVIDER",
13
15
  "PI_MODEL",
14
16
  "PI_REASONING_LEVEL",
@@ -12,6 +12,8 @@
12
12
  export const SESSION_ENVIRONMENT_KEYS = [
13
13
  "PI_SESSION_ID",
14
14
  "PI_SESSION_FILE",
15
+ "PI_SESSION_CWD",
16
+ "PI_GOAL_STORE_FILE",
15
17
  "PI_PROVIDER",
16
18
  "PI_MODEL",
17
19
  "PI_REASONING_LEVEL",
@@ -22,6 +24,8 @@ export type SessionEnvironment = Readonly<Record<string, string>>;
22
24
 
23
25
  /** Structural slice of `ExtensionContext` the session environment is resolved from. */
24
26
  export interface SessionEnvironmentSource {
27
+ readonly cwd: string;
28
+ readonly goalStoreFile?: string;
25
29
  readonly sessionManager: {
26
30
  getSessionId(): string;
27
31
  getSessionFile(): string | undefined;
@@ -33,6 +37,8 @@ export interface SessionEnvironmentSource {
33
37
  export function sessionEnvironmentFrom(source: SessionEnvironmentSource): SessionEnvironment {
34
38
  const env: Record<string, string> = {};
35
39
  env.PI_SESSION_ID = source.sessionManager.getSessionId();
40
+ env.PI_SESSION_CWD = source.cwd;
41
+ if (source.goalStoreFile) env.PI_GOAL_STORE_FILE = source.goalStoreFile;
36
42
  const sessionFile = source.sessionManager.getSessionFile();
37
43
  if (sessionFile) env.PI_SESSION_FILE = sessionFile;
38
44
  const model = source.model;
@@ -144,7 +144,12 @@ export class EvalDetachedCellManager {
144
144
  }
145
145
 
146
146
  detach(cell: ManagedCell): boolean {
147
- if (!cell.canDetach || !allowsDetachedCellTransition(cell.state, "detached")) return false;
147
+ if (
148
+ !cell.canDetach ||
149
+ !allowsDetachedCellTransition(cell.state, "detached") ||
150
+ this.#detachedByLanguage.has(cell.input.language)
151
+ )
152
+ return false;
148
153
  cell.state = "detached";
149
154
  cell.wasDetached = true;
150
155
  this.#detachedByLanguage.set(cell.input.language, cell);
@@ -49,6 +49,12 @@ export async function runEvalCell(
49
49
  let detached = false;
50
50
  let execution: CellExecution;
51
51
  const cell = cellManager.create(invocation.cellId, invocation.input, (error) => execution.cancel(error));
52
+ const detach = (): boolean => {
53
+ if (!cellManager.detach(cell)) return false;
54
+ detached = true;
55
+ execution.detach();
56
+ return true;
57
+ };
52
58
  execution = new CellExecution({
53
59
  callerSignal: invocation.signal,
54
60
  cellId: invocation.cellId,
@@ -58,12 +64,7 @@ export async function runEvalCell(
58
64
  timeoutMs: detachAfterMs,
59
65
  maxPauseGraceMs: foregroundWindowMs,
60
66
  onTimeout: (error: Error) => {
61
- if (cellManager.detach(cell)) {
62
- detached = true;
63
- execution.detach();
64
- return;
65
- }
66
- execution.cancel(error);
67
+ if (!detach()) execution.cancel(error);
67
68
  },
68
69
  },
69
70
  }
@@ -74,6 +75,16 @@ export async function runEvalCell(
74
75
  bridgeAbortController.abort(error);
75
76
  },
76
77
  });
78
+ const steeringSignal =
79
+ detaches && invocation.ctx.mode !== "print" && invocation.ctx.mode !== "json"
80
+ ? invocation.ctx.steeringSignal
81
+ : undefined;
82
+ const onSteering = (): void => {
83
+ if (!steeringSignal?.aborted || detached || !state.active || invocation.signal.aborted) return;
84
+ // Losing the slot keeps this call foreground; only the existing deadlines/caller may cancel it.
85
+ detach();
86
+ };
87
+ steeringSignal?.addEventListener("abort", onSteering, { once: true });
77
88
  const running = executeCell(
78
89
  options,
79
90
  invocation,
@@ -83,6 +94,7 @@ export async function runEvalCell(
83
94
  execution,
84
95
  bridgeContext,
85
96
  bridgeAbortController,
97
+ onSteering,
86
98
  );
87
99
  let settleEventEmitted = false;
88
100
  const emitSettled = (outcome: EvalExecutionSettleOutcome): void => {
@@ -110,12 +122,16 @@ export async function runEvalCell(
110
122
  throw error;
111
123
  },
112
124
  );
113
- const outcome = await Promise.race([
114
- finalized.then((result) => ({ kind: "result" as const, result })),
115
- execution.detached.then(() => ({ kind: "detached" as const })),
116
- ]);
117
- if (outcome.kind === "detached") return resultAfterDetach(cellManager.peek(invocation.cellId), invocation.input);
118
- return outcome.result;
125
+ try {
126
+ const outcome = await Promise.race([
127
+ finalized.then((result) => ({ kind: "result" as const, result })),
128
+ execution.detached.then(() => ({ kind: "detached" as const })),
129
+ ]);
130
+ if (outcome.kind === "detached") return resultAfterDetach(cellManager.peek(invocation.cellId), invocation.input);
131
+ return outcome.result;
132
+ } finally {
133
+ steeringSignal?.removeEventListener("abort", onSteering);
134
+ }
119
135
  }
120
136
 
121
137
  async function executeCell(
@@ -127,6 +143,7 @@ async function executeCell(
127
143
  execution: CellExecution,
128
144
  bridgeContext: ExtensionContext,
129
145
  bridgeAbortController: AbortController,
146
+ onReady: () => void,
130
147
  ): Promise<AgentToolResult<EvalToolDetails>> {
131
148
  let handler: CellHandler | undefined;
132
149
  try {
@@ -168,6 +185,8 @@ async function executeCell(
168
185
  () => activeHandler.liveResult(),
169
186
  (error) => execution.cancel(error),
170
187
  );
188
+ // Includes a steer already queued at execute start or received while acquiring the kernel.
189
+ onReady();
171
190
  if ("setContext" in options.kernelManager && typeof options.kernelManager.setContext === "function") {
172
191
  options.kernelManager.setContext(bridgeContext);
173
192
  }