@ian-pascoe/pi-codemode 0.1.0 → 0.3.0

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/README.md CHANGED
@@ -62,11 +62,44 @@ codemode_cancel({ sessionId: string });
62
62
  Stops the session process and frees its capacity. The cancel call succeeds;
63
63
  subsequent polling returns the retained `cancellation` failure.
64
64
 
65
- All three tools return:
65
+ ### `codemode_sessions`
66
66
 
67
67
  ```ts
68
+ codemode_sessions({});
69
+ ```
70
+
71
+ Lists every live Session, with idle Sessions first in least-recently-used order,
72
+ then running Sessions in least-recently-used order. Listing does not refresh
73
+ Session recency.
74
+
75
+ ```ts
76
+ type CodeModeSessionsResult = {
77
+ result: "success";
78
+ sessions: Array<{
79
+ sessionId: string;
80
+ state: "idle" | "running";
81
+ cellCount: number;
82
+ lastActivityAtMs: number;
83
+ }>;
84
+ };
85
+ ```
86
+
87
+ The execute, result, and cancel tools return:
88
+
89
+ ```ts
90
+ type CodeModeConsoleEntry = {
91
+ method: "log" | "info" | "warn" | "error" | "debug";
92
+ text: string;
93
+ };
94
+
68
95
  type CodeModeResult =
69
- | { result: "success"; sessionId: string; data?: JsonValue }
96
+ | {
97
+ result: "success";
98
+ sessionId: string;
99
+ data?: JsonValue;
100
+ reclaimedSessionId?: string;
101
+ console?: CodeModeConsoleEntry[];
102
+ }
70
103
  | { result: "pending"; sessionId: string }
71
104
  | {
72
105
  result: "failed";
@@ -76,6 +109,7 @@ type CodeModeResult =
76
109
  | "unknown"
77
110
  | "busy"
78
111
  | "capacity"
112
+ | "eviction"
79
113
  | "script"
80
114
  | "serialization"
81
115
  | "timeout"
@@ -84,6 +118,7 @@ type CodeModeResult =
84
118
  | "runtime";
85
119
  message: string;
86
120
  };
121
+ console?: CodeModeConsoleEntry[];
87
122
  };
88
123
  ```
89
124
 
@@ -91,29 +126,57 @@ type CodeModeResult =
91
126
  result text returned to the model. Pi retains additional bounded Presentation
92
127
  Snapshots in tool-result details for Transcript replay and the TUI.
93
128
 
129
+ Cells may call `console.log`, `console.info`, `console.warn`, `console.error`,
130
+ and `console.debug`. One call creates one ordered entry without a trailing
131
+ newline, while embedded newlines remain intact:
132
+
133
+ ```ts
134
+ console.log("answer:", 42);
135
+ return 42;
136
+ ```
137
+
138
+ ```json
139
+ {
140
+ "result": "success",
141
+ "sessionId": "...",
142
+ "data": 42,
143
+ "console": [{ "method": "log", "text": "answer: 42" }]
144
+ }
145
+ ```
146
+
147
+ Formatting matches the pinned Deno Console for format tokens, primitives,
148
+ spacing, and multiline inspection. Getters, coercion hooks, and custom
149
+ inspectors do not run; CodeMode uses safe inspection instead when that differs
150
+ from Deno. Console output arrives only with terminal results. Ordinary script,
151
+ serialization, and worker-reported runtime failures retain prior calls. A
152
+ timeout, cancellation, termination, or process death may omit them because the
153
+ parent kills the worker before it can reply.
154
+
94
155
  ## Transcript and Observer UI
95
156
 
96
- The CodeMode Transcript gives all three tools semantic collapsed and expanded
157
+ The CodeMode Transcript gives all four tools semantic collapsed and expanded
97
158
  rendering. Collapsed rows prioritize Cell lifecycle, a short Session ID, Cell
98
- Ordinal, returned-value shape, nested-tool count, and elapsed time. Expanded
99
- rows show the full Session ID, explicit call arguments, TypeScript source,
100
- structured returned data or error, and bounded nested-tool names, outcomes, and
101
- durations. Nested arguments and raw nested outputs are never copied into the
102
- presentation.
159
+ Ordinal, returned-value shape, Console-call count, nested-tool count, and elapsed
160
+ time. Expanded rows show the full Session ID, explicit call arguments,
161
+ TypeScript source, bounded Console output before structured returned data or the
162
+ error, and bounded nested-tool names, outcomes, and durations. Nested arguments
163
+ and raw nested outputs are never copied into the presentation.
103
164
 
104
165
  Status always uses a symbol and text together:
105
166
 
106
167
  ```text
107
168
  ◉ running ○ idle ✓ completed
108
- × failed ■ cancelled ! timed out
169
+ × failed ■ cancelled ■ reclaimed ! timed out
109
170
  ```
110
171
 
111
172
  Awaited Cells publish a presentation update immediately and once per second.
112
- Source display is limited to 200 lines or 50 KB. Returned-data display uses
113
- Pi's 2,000-line/50-KB limit; complete oversized data is written to a private
114
- Result Spill while the model-facing result remains unchanged. Result Spill
115
- files last for the live Pi session. Replayed history falls back to its retained
116
- bounded data when a prior spill is no longer available.
173
+ Collapsed calls show one highlighted line inline or the first eight highlighted
174
+ lines of a multi-line Cell. Expanded calls show the complete TypeScript source.
175
+ Returned-data display uses Pi's 2,000-line/50-KB limit; complete oversized data
176
+ is written to a private Result Spill while the model-facing result remains
177
+ unchanged. Result Spill files last for the live Pi session. Replayed history
178
+ falls back to its retained bounded data when a prior spill is no longer
179
+ available.
117
180
 
118
181
  In TUI mode, the read-only CodeMode Observer UI appears above the editor during
119
182
  Cell activity. It shows up to eight running, idle, or recently terminal
@@ -204,12 +267,18 @@ last match wins. Project `tools` replaces the global array, while project
204
267
 
205
268
  An unmatched active tool defaults to `direct-and-codemode`; an unmatched
206
269
  inactive tool remains unavailable. An explicit rule may expose an inactive tool
207
- or activate direct access. The three `codemode_*` tools are always direct-only.
270
+ or activate direct access. The four `codemode_*` tools are always direct-only.
208
271
  Pi's global allowed/excluded registry remains authoritative. Invalid fields or
209
272
  patterns disable CodeMode for that session without changing Pi's active tools.
210
273
 
211
- `maxSessions` defaults to 8 and counts only live Deno processes. Up to 64 recent
212
- worker-free terminal or failed-admission records remain pollable.
274
+ `maxSessions` defaults to 8 and counts only live Deno processes. When capacity
275
+ is full, a new Session gracefully stops the least-recently-used idle Session
276
+ before starting; its Notebook Bindings are discarded, the new success reports
277
+ `reclaimedSessionId`, and polling the old ID returns `eviction`. Running Sessions
278
+ are never reclaimed, so admission still returns `capacity` when every process is
279
+ busy. Executing or polling refreshes recency; listing and Observer rendering do
280
+ not. Up to 64 recent worker-free terminal or failed-admission records remain
281
+ pollable.
213
282
 
214
283
  ## Isolation and limits
215
284
 
@@ -219,13 +288,16 @@ generated helper source out of ordinary source locations. Every operating-system
219
288
  permission class is denied: filesystem read/write, network, environment, system
220
289
  information, subprocesses, FFI, and remote imports.
221
290
 
222
- Guest code receives ECMAScript built-ins, a read-only `tools` object, and only a
223
- frozen `Deno.version` identity. Raw process and standard-stream access,
224
- `console`, `Worker`, timers, filesystem/network APIs, and module loading are
225
- withheld. The parent watchdog terminates the subprocess for timeout or an
226
- infinite loop. Deno/V8 bounds each Session to a 128 MiB old-space heap and a
227
- 1 MiB stack. Protocol inputs, tool results, and Cell results remain JSON-only
228
- and limited to 8 MiB of UTF-8.
291
+ Guest code receives ECMAScript built-ins, a read-only `tools` object, a frozen
292
+ five-method Console facade, and only a frozen `Deno.version` identity. Raw
293
+ process and standard-stream access, `Worker`, timers, filesystem/network APIs,
294
+ and module loading are withheld. Console calls never write to worker streams and
295
+ do not include output from registered Pi tool handlers. The parent watchdog
296
+ terminates the subprocess for timeout or an infinite loop. Deno/V8 bounds each
297
+ Session to a 128 MiB old-space heap and a 1 MiB stack. Protocol inputs, tool
298
+ results, returned data, and Console entries share one 8 MiB UTF-8 worker-message
299
+ limit. An oversized response becomes the bounded `serialization` failure and
300
+ may omit its Console entries.
229
301
 
230
302
  Registered Pi tools still execute in Pi's parent process with their normal
231
303
  permissions and lifecycle hooks. Cancellation aborts them through Pi's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-codemode",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Process-isolated persistent TypeScript tool composition for Pi",
6
6
  "keywords": [
@@ -32,10 +32,6 @@
32
32
  "access": "public",
33
33
  "provenance": true
34
34
  },
35
- "scripts": {
36
- "test": "vitest run --config ../../vitest.config.ts --root .",
37
- "typecheck": "tsc --noEmit -p tsconfig.json"
38
- },
39
35
  "dependencies": {
40
36
  "deno": "2.9.5",
41
37
  "minimatch": "^10.2.5",
@@ -55,5 +51,9 @@
55
51
  "extensions": [
56
52
  "./src/index.ts"
57
53
  ]
54
+ },
55
+ "scripts": {
56
+ "test": "vitest run --config ../../vitest.config.ts --root .",
57
+ "typecheck": "tsc --noEmit -p tsconfig.json"
58
58
  }
59
- }
59
+ }
@@ -0,0 +1,11 @@
1
+ /** Console methods captured from a CodeMode Cell in call order. */
2
+ export const CODEMODE_CONSOLE_METHODS = ["log", "info", "warn", "error", "debug"] as const;
3
+
4
+ /** One supported CodeMode Cell Console method. */
5
+ export type CodeModeConsoleMethod = (typeof CODEMODE_CONSOLE_METHODS)[number];
6
+
7
+ /** One captured CodeMode Cell Console call without a synthetic trailing newline. */
8
+ export type CodeModeConsoleEntry = {
9
+ readonly method: CodeModeConsoleMethod;
10
+ readonly text: string;
11
+ };
@@ -12,6 +12,11 @@ import type {
12
12
  CodeModeObserverSnapshot,
13
13
  CodeModeUnexpectedFailure,
14
14
  } from "./codemode-session-coordinator.js";
15
+ import {
16
+ boundedCodeModeElapsedMs,
17
+ formatCodeModeDuration,
18
+ shortestUniqueCodeModeSessionPrefix,
19
+ } from "./codemode-session-coordinator.js";
15
20
 
16
21
  const CODEMODE_OBSERVER_UI_KEY = "codemode-observer";
17
22
  const CODEMODE_OBSERVER_ROW_LIMIT = 8;
@@ -72,7 +77,7 @@ export type CodeModeObserverRow = {
72
77
  /** Idle Session summary retained only during relevant Observer activity. */
73
78
  | { readonly state: "idle"; readonly cellCount: number }
74
79
  | {
75
- readonly state: "failed" | "cancelled" | "timed_out";
80
+ readonly state: "failed" | "cancelled" | "reclaimed" | "timed_out";
76
81
  /** Most recent one-based Cell Ordinal, or Session Cell count when no final Cell exists. */
77
82
  readonly cellOrdinal: number;
78
83
  }
@@ -102,6 +107,7 @@ const CODEMODE_OBSERVER_STATE_PRESENTATION = {
102
107
  idle: { symbol: "○", label: "idle", color: "muted" },
103
108
  failed: { symbol: "×", label: "failed", color: "error" },
104
109
  cancelled: { symbol: "■", label: "cancelled", color: "warning" },
110
+ reclaimed: { symbol: "■", label: "reclaimed", color: "warning" },
105
111
  timed_out: { symbol: "!", label: "timed out", color: "error" },
106
112
  } satisfies Record<CodeModeObserverState, CodeModeObserverStatePresentation>;
107
113
 
@@ -130,33 +136,21 @@ function shortestUniqueCodeModeSessionPrefixes(
130
136
  const safe = sanitizeCodeModeObserverText(session.sessionId);
131
137
  return safe.length === 0 ? "unknown" : safe;
132
138
  });
133
- return identifiers.map((identifier, index) => {
134
- let length = Math.min(8, identifier.length);
135
- while (
136
- length < identifier.length &&
137
- identifiers.some(
138
- (candidate, candidateIndex) =>
139
- candidateIndex !== index && candidate.startsWith(identifier.slice(0, length)),
140
- )
141
- ) {
142
- length += 1;
143
- }
144
- return identifier.slice(0, length);
145
- });
146
- }
147
-
148
- function boundedElapsedMilliseconds(nowMs: number, startedAtMs: number): number {
149
- const elapsed = Math.round(nowMs - startedAtMs);
150
- if (!Number.isFinite(elapsed)) return 0;
151
- return Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, elapsed));
139
+ return identifiers.map((identifier, index) =>
140
+ shortestUniqueCodeModeSessionPrefix(
141
+ identifier,
142
+ identifiers.filter((_candidate, candidateIndex) => candidateIndex !== index),
143
+ ),
144
+ );
152
145
  }
153
146
 
154
147
  function codeModeTerminalObserverState(
155
148
  session: CodeModeObservedSession,
156
- ): Extract<CodeModeObserverState, "failed" | "cancelled" | "timed_out"> {
149
+ ): Extract<CodeModeObserverState, "failed" | "cancelled" | "reclaimed" | "timed_out"> {
157
150
  if (session.terminal_error_code === "cancellation" || session.last_cell?.state === "cancelled") {
158
151
  return "cancelled";
159
152
  }
153
+ if (session.terminal_error_code === "eviction") return "reclaimed";
160
154
  if (session.terminal_error_code === "timeout" || session.last_cell?.state === "timed_out") {
161
155
  return "timed_out";
162
156
  }
@@ -180,7 +174,7 @@ function projectCodeModeObserverRow(
180
174
  sessionPrefix,
181
175
  state: "running",
182
176
  cellOrdinal: session.current_cell.ordinal,
183
- elapsedMs: boundedElapsedMilliseconds(nowMs, session.current_cell.started_at_ms),
177
+ elapsedMs: boundedCodeModeElapsedMs(session.current_cell.started_at_ms, nowMs),
184
178
  activeToolNames,
185
179
  activeToolCount: Math.max(session.current_cell.active_tool_count, activeToolNames.length),
186
180
  };
@@ -230,13 +224,6 @@ export function buildCodeModeObserverView(
230
224
  };
231
225
  }
232
226
 
233
- function formatCodeModeObserverDuration(elapsedMs: number): string {
234
- if (elapsedMs < 1_000) return `${elapsedMs}ms`;
235
- if (elapsedMs < 60_000) return `${(elapsedMs / 1_000).toFixed(1)}s`;
236
- const seconds = Math.floor(elapsedMs / 1_000);
237
- return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
238
- }
239
-
240
227
  function formatCodeModeObserverToolActivity(
241
228
  row: Extract<CodeModeObserverRow, { state: "running" }>,
242
229
  ): string | undefined {
@@ -254,10 +241,6 @@ function codeModeObserverRowDetail(row: CodeModeObserverRow): string {
254
241
  : `Cell ${row.cellOrdinal}`;
255
242
  }
256
243
 
257
- function joinCodeModeObserverRow(parts: readonly string[], separator: string): string {
258
- return parts.join(separator);
259
- }
260
-
261
244
  function renderCodeModeObserverRow(
262
245
  row: CodeModeObserverRow,
263
246
  width: number,
@@ -268,9 +251,7 @@ function renderCodeModeObserverRow(
268
251
  const status = theme.fg(presentation.color, presentation.label);
269
252
  const detail = theme.fg("muted", codeModeObserverRowDetail(row));
270
253
  const duration =
271
- row.state === "running"
272
- ? theme.fg("muted", formatCodeModeObserverDuration(row.elapsedMs))
273
- : undefined;
254
+ row.state === "running" ? theme.fg("muted", formatCodeModeDuration(row.elapsedMs)) : undefined;
274
255
  const toolActivity =
275
256
  row.state === "running" ? formatCodeModeObserverToolActivity(row) : undefined;
276
257
  const themedToolActivity =
@@ -283,13 +264,13 @@ function renderCodeModeObserverRow(
283
264
  [identity, status],
284
265
  ].map((parts) => parts.filter((part): part is string => part !== undefined));
285
266
  for (const parts of candidates) {
286
- const line = joinCodeModeObserverRow(parts, separator);
267
+ const line = parts.join(separator);
287
268
  if (visibleWidth(line) <= width) return line;
288
269
  }
289
270
 
290
271
  const compactSeparator = theme.fg("dim", CODEMODE_OBSERVER_COMPACT_SEPARATOR_TEXT);
291
272
  const compactIdentity = `${theme.fg(presentation.color, presentation.symbol)} ${theme.bold(row.sessionPrefix)}`;
292
- const compact = joinCodeModeObserverRow([compactIdentity, status], compactSeparator);
273
+ const compact = [compactIdentity, status].join(compactSeparator);
293
274
  if (visibleWidth(compact) <= width) return compact;
294
275
  return truncateToWidth(compact, width, CODEMODE_OBSERVER_ELLIPSIS);
295
276
  }