@ian-pascoe/pi-codemode 0.2.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
@@ -87,12 +87,18 @@ type CodeModeSessionsResult = {
87
87
  The execute, result, and cancel tools return:
88
88
 
89
89
  ```ts
90
+ type CodeModeConsoleEntry = {
91
+ method: "log" | "info" | "warn" | "error" | "debug";
92
+ text: string;
93
+ };
94
+
90
95
  type CodeModeResult =
91
96
  | {
92
97
  result: "success";
93
98
  sessionId: string;
94
99
  data?: JsonValue;
95
100
  reclaimedSessionId?: string;
101
+ console?: CodeModeConsoleEntry[];
96
102
  }
97
103
  | { result: "pending"; sessionId: string }
98
104
  | {
@@ -112,6 +118,7 @@ type CodeModeResult =
112
118
  | "runtime";
113
119
  message: string;
114
120
  };
121
+ console?: CodeModeConsoleEntry[];
115
122
  };
116
123
  ```
117
124
 
@@ -119,15 +126,41 @@ type CodeModeResult =
119
126
  result text returned to the model. Pi retains additional bounded Presentation
120
127
  Snapshots in tool-result details for Transcript replay and the TUI.
121
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
+
122
155
  ## Transcript and Observer UI
123
156
 
124
157
  The CodeMode Transcript gives all four tools semantic collapsed and expanded
125
158
  rendering. Collapsed rows prioritize Cell lifecycle, a short Session ID, Cell
126
- Ordinal, returned-value shape, nested-tool count, and elapsed time. Expanded
127
- rows show the full Session ID, explicit call arguments, TypeScript source,
128
- structured returned data or error, and bounded nested-tool names, outcomes, and
129
- durations. Nested arguments and raw nested outputs are never copied into the
130
- 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.
131
164
 
132
165
  Status always uses a symbol and text together:
133
166
 
@@ -255,13 +288,16 @@ generated helper source out of ordinary source locations. Every operating-system
255
288
  permission class is denied: filesystem read/write, network, environment, system
256
289
  information, subprocesses, FFI, and remote imports.
257
290
 
258
- Guest code receives ECMAScript built-ins, a read-only `tools` object, and only a
259
- frozen `Deno.version` identity. Raw process and standard-stream access,
260
- `console`, `Worker`, timers, filesystem/network APIs, and module loading are
261
- withheld. The parent watchdog terminates the subprocess for timeout or an
262
- infinite loop. Deno/V8 bounds each Session to a 128 MiB old-space heap and a
263
- 1 MiB stack. Protocol inputs, tool results, and Cell results remain JSON-only
264
- 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.
265
301
 
266
302
  Registered Pi tools still execute in Pi's parent process with their normal
267
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.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Process-isolated persistent TypeScript tool composition for Pi",
6
6
  "keywords": [
@@ -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;
@@ -131,25 +136,12 @@ function shortestUniqueCodeModeSessionPrefixes(
131
136
  const safe = sanitizeCodeModeObserverText(session.sessionId);
132
137
  return safe.length === 0 ? "unknown" : safe;
133
138
  });
134
- return identifiers.map((identifier, index) => {
135
- let length = Math.min(8, identifier.length);
136
- while (
137
- length < identifier.length &&
138
- identifiers.some(
139
- (candidate, candidateIndex) =>
140
- candidateIndex !== index && candidate.startsWith(identifier.slice(0, length)),
141
- )
142
- ) {
143
- length += 1;
144
- }
145
- return identifier.slice(0, length);
146
- });
147
- }
148
-
149
- function boundedElapsedMilliseconds(nowMs: number, startedAtMs: number): number {
150
- const elapsed = Math.round(nowMs - startedAtMs);
151
- if (!Number.isFinite(elapsed)) return 0;
152
- 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
+ );
153
145
  }
154
146
 
155
147
  function codeModeTerminalObserverState(
@@ -182,7 +174,7 @@ function projectCodeModeObserverRow(
182
174
  sessionPrefix,
183
175
  state: "running",
184
176
  cellOrdinal: session.current_cell.ordinal,
185
- elapsedMs: boundedElapsedMilliseconds(nowMs, session.current_cell.started_at_ms),
177
+ elapsedMs: boundedCodeModeElapsedMs(session.current_cell.started_at_ms, nowMs),
186
178
  activeToolNames,
187
179
  activeToolCount: Math.max(session.current_cell.active_tool_count, activeToolNames.length),
188
180
  };
@@ -232,13 +224,6 @@ export function buildCodeModeObserverView(
232
224
  };
233
225
  }
234
226
 
235
- function formatCodeModeObserverDuration(elapsedMs: number): string {
236
- if (elapsedMs < 1_000) return `${elapsedMs}ms`;
237
- if (elapsedMs < 60_000) return `${(elapsedMs / 1_000).toFixed(1)}s`;
238
- const seconds = Math.floor(elapsedMs / 1_000);
239
- return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
240
- }
241
-
242
227
  function formatCodeModeObserverToolActivity(
243
228
  row: Extract<CodeModeObserverRow, { state: "running" }>,
244
229
  ): string | undefined {
@@ -256,10 +241,6 @@ function codeModeObserverRowDetail(row: CodeModeObserverRow): string {
256
241
  : `Cell ${row.cellOrdinal}`;
257
242
  }
258
243
 
259
- function joinCodeModeObserverRow(parts: readonly string[], separator: string): string {
260
- return parts.join(separator);
261
- }
262
-
263
244
  function renderCodeModeObserverRow(
264
245
  row: CodeModeObserverRow,
265
246
  width: number,
@@ -270,9 +251,7 @@ function renderCodeModeObserverRow(
270
251
  const status = theme.fg(presentation.color, presentation.label);
271
252
  const detail = theme.fg("muted", codeModeObserverRowDetail(row));
272
253
  const duration =
273
- row.state === "running"
274
- ? theme.fg("muted", formatCodeModeObserverDuration(row.elapsedMs))
275
- : undefined;
254
+ row.state === "running" ? theme.fg("muted", formatCodeModeDuration(row.elapsedMs)) : undefined;
276
255
  const toolActivity =
277
256
  row.state === "running" ? formatCodeModeObserverToolActivity(row) : undefined;
278
257
  const themedToolActivity =
@@ -285,13 +264,13 @@ function renderCodeModeObserverRow(
285
264
  [identity, status],
286
265
  ].map((parts) => parts.filter((part): part is string => part !== undefined));
287
266
  for (const parts of candidates) {
288
- const line = joinCodeModeObserverRow(parts, separator);
267
+ const line = parts.join(separator);
289
268
  if (visibleWidth(line) <= width) return line;
290
269
  }
291
270
 
292
271
  const compactSeparator = theme.fg("dim", CODEMODE_OBSERVER_COMPACT_SEPARATOR_TEXT);
293
272
  const compactIdentity = `${theme.fg(presentation.color, presentation.symbol)} ${theme.bold(row.sessionPrefix)}`;
294
- const compact = joinCodeModeObserverRow([compactIdentity, status], compactSeparator);
273
+ const compact = [compactIdentity, status].join(compactSeparator);
295
274
  if (visibleWidth(compact) <= width) return compact;
296
275
  return truncateToWidth(compact, width, CODEMODE_OBSERVER_ELLIPSIS);
297
276
  }
@@ -2,6 +2,7 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
2
  import type { Usage } from "@earendil-works/pi-ai";
3
3
  import { truncateHead } from "@earendil-works/pi-coding-agent";
4
4
  import { transformCodeModeCell } from "./codemode-cell-transform.js";
5
+ import type { CodeModeConsoleEntry } from "./codemode-console-output.js";
5
6
  import { CodeModeWorkerProcess } from "./codemode-deno-process.js";
6
7
  import { formatCodeModePresentationData } from "./codemode-presentation-output.js";
7
8
  import type { CodeModeRuntime, CodeModeTimerHandle } from "./codemode-runtime.js";
@@ -35,9 +36,6 @@ const CODEMODE_RESULT_PRESENTATION_MAX_LINES = 2_000;
35
36
  const CODEMODE_WATCHDOG_GRACE_MS = 100;
36
37
  const INVALID_CODEMODE_SESSION_ID = "invalid-session-id";
37
38
 
38
- /** Branded identifier for one retained CodeMode Session. */
39
- export type CodeModeSessionId = string & { readonly CodeModeSessionId: unique symbol };
40
-
41
39
  /** Observer state of one settled CodeMode Cell. */
42
40
  export type CodeModeObservedCellState = "completed" | "failed" | "cancelled" | "timed_out";
43
41
 
@@ -70,7 +68,7 @@ export type CodeModeObservedLastCell = {
70
68
 
71
69
  /** Immutable read-only facts for one CodeMode Session with at least one Cell. */
72
70
  export type CodeModeObservedSession = {
73
- readonly sessionId: CodeModeSessionId;
71
+ readonly sessionId: string;
74
72
  readonly lifecycle: "running" | "idle" | "terminal";
75
73
  /** Number of Cells started in this CodeMode Session. */
76
74
  readonly cell_count: number;
@@ -88,7 +86,7 @@ export type CodeModeObserverSnapshot = {
88
86
 
89
87
  /** Read-only facts for one live CodeMode Session, ordered by admission priority. */
90
88
  export type CodeModeListedSession = {
91
- readonly sessionId: CodeModeSessionId;
89
+ readonly sessionId: string;
92
90
  readonly state: "running" | "idle";
93
91
  readonly cellCount: number;
94
92
  /** Parent wall-clock time of the latest execution-visible transition in Unix-epoch milliseconds. */
@@ -97,7 +95,7 @@ export type CodeModeListedSession = {
97
95
 
98
96
  /** One idle CodeMode worker failure not represented by an active Cell result. */
99
97
  export type CodeModeUnexpectedFailure = {
100
- readonly sessionId: CodeModeSessionId;
98
+ readonly sessionId: string;
101
99
  readonly message: string;
102
100
  };
103
101
 
@@ -122,7 +120,7 @@ export type CodeModeNestedToolUpdate = AgentToolResult<unknown>;
122
120
 
123
121
  /** One complete guest batch plus its Cell-scoped cancellation and update capabilities. */
124
122
  export type CodeModeNestedToolBatch = {
125
- readonly sessionId: CodeModeSessionId;
123
+ readonly sessionId: string;
126
124
  readonly batchId: string;
127
125
  readonly calls: readonly CodeModeNestedToolCall[];
128
126
  readonly signal: AbortSignal;
@@ -187,14 +185,14 @@ type MutableCodeModeOuterToolMetadata = {
187
185
 
188
186
  type CreateActiveCodeModeCellOptions = {
189
187
  onUpdate?: (update: CodeModeNestedToolUpdate) => void;
190
- reclaimedSessionId?: CodeModeSessionId;
188
+ reclaimedSessionId?: string;
191
189
  };
192
190
 
193
191
  type ActiveCodeModeCell = {
194
192
  readonly cellId: string;
195
193
  readonly ordinal: number;
196
194
  readonly startedAtMs: number;
197
- readonly reclaimedSessionId?: CodeModeSessionId;
195
+ readonly reclaimedSessionId?: string;
198
196
  readonly abortController: AbortController;
199
197
  readonly completion: Promise<CodeModeResult>;
200
198
  readonly resolveCompletion: (result: CodeModeResult) => void;
@@ -213,7 +211,7 @@ type ActiveCodeModeCell = {
213
211
 
214
212
  type LiveCodeModeSession = {
215
213
  readonly state: "live";
216
- readonly sessionId: CodeModeSessionId;
214
+ readonly sessionId: string;
217
215
  readonly worker: CodeModeWorkerProcess;
218
216
  lastAccess: number;
219
217
  lastActivityAtMs: number;
@@ -227,7 +225,7 @@ type LiveCodeModeSession = {
227
225
 
228
226
  type TerminalCodeModeSession = {
229
227
  readonly state: "terminal";
230
- readonly sessionId: CodeModeSessionId;
228
+ readonly sessionId: string;
231
229
  lastAccess: number;
232
230
  readonly lastActivityAtMs: number;
233
231
  readonly cellCount: number;
@@ -242,23 +240,50 @@ type CodeModeSessionRecord = LiveCodeModeSession | TerminalCodeModeSession;
242
240
  type LocateCodeModeSessionResult =
243
241
  | {
244
242
  readonly record: CodeModeSessionRecord;
245
- readonly reclaimedSessionId?: CodeModeSessionId;
243
+ readonly reclaimedSessionId?: string;
246
244
  }
247
245
  | { readonly failure: CodeModeResult };
248
246
 
249
247
  type FatalCodeModeSessionFailure = {
250
248
  readonly code: Extract<CodeModeErrorCode, "timeout" | "cancellation" | "termination" | "runtime">;
251
249
  readonly message: string;
250
+ readonly console?: readonly CodeModeConsoleEntry[];
252
251
  };
253
252
 
254
- type ParseCodeModeSessionIdResult =
255
- | { readonly ok: true; readonly value: CodeModeSessionId }
256
- | { readonly ok: false };
253
+ type ParsestringResult = { readonly ok: true; readonly value: string } | { readonly ok: false };
254
+
255
+ function parsestring(value: string): ParsestringResult {
256
+ return value.length === 0 ? { ok: false } : { ok: true, value };
257
+ }
258
+
259
+ /** Return the shortest prefix of the identifier that no candidate shares. */
260
+ export function shortestUniqueCodeModeSessionPrefix(
261
+ identifier: string,
262
+ candidates: readonly string[],
263
+ ): string {
264
+ let length = Math.min(8, identifier.length);
265
+ while (
266
+ length < identifier.length &&
267
+ candidates.some((candidate) => candidate.startsWith(identifier.slice(0, length)))
268
+ ) {
269
+ length += 1;
270
+ }
271
+ return identifier.slice(0, length);
272
+ }
257
273
 
258
- function parseCodeModeSessionId(value: string): ParseCodeModeSessionIdResult {
259
- if (value.length === 0) return { ok: false };
260
- // SAFETY: This parser establishes the only CodeMode Session ID invariant (non-empty) before applying the domain brand.
261
- return { ok: true, value: value as CodeModeSessionId };
274
+ /** Render one elapsed duration as ms, seconds, or minutes + padded seconds. */
275
+ export function formatCodeModeDuration(elapsedMs: number): string {
276
+ if (elapsedMs < 1_000) return `${elapsedMs}ms`;
277
+ if (elapsedMs < 60_000) return `${(elapsedMs / 1_000).toFixed(1)}s`;
278
+ const seconds = Math.floor(elapsedMs / 1_000);
279
+ return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
280
+ }
281
+
282
+ /** Round one elapsed measurement and clamp it to a safe non-negative integer. */
283
+ export function boundedCodeModeElapsedMs(startedAtMs: number, observedAtMs: number): number {
284
+ const elapsed = Math.round(observedAtMs - startedAtMs);
285
+ if (!Number.isFinite(elapsed)) return 0;
286
+ return Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, elapsed));
262
287
  }
263
288
 
264
289
  function invalidCodeModeSessionResult(): CodeModeSessionOperationResult {
@@ -275,12 +300,11 @@ function emptyMetadataAccumulator(): CodeModeMetadataAccumulator {
275
300
  return { addedToolNames: new Set(), terminate: false };
276
301
  }
277
302
 
278
- function combineCodeModeUsage(left: Usage | undefined, right: Usage): Usage {
303
+ /** Sums two Usage values; optional fields appear only when either side carries them. */
304
+ export function addCodeModeUsage(left: Usage | undefined, right: Usage | undefined): Usage {
305
+ if (right === undefined) throw new Error("Pi CodeMode usage: missing increment");
279
306
  if (left === undefined) {
280
- return {
281
- ...right,
282
- cost: { ...right.cost },
283
- };
307
+ return { ...right, cost: { ...right.cost } };
284
308
  }
285
309
  const combined: Usage = {
286
310
  input: left.input + right.input,
@@ -310,7 +334,7 @@ function mergeCodeModeOuterMetadata(
310
334
  metadata: CodeModeOuterToolMetadata,
311
335
  ): void {
312
336
  if (metadata.usage !== undefined) {
313
- accumulator.usage = combineCodeModeUsage(accumulator.usage, metadata.usage);
337
+ accumulator.usage = addCodeModeUsage(accumulator.usage, metadata.usage);
314
338
  }
315
339
  for (const name of metadata.addedToolNames ?? []) accumulator.addedToolNames.add(name);
316
340
  if (metadata.terminate === true) accumulator.terminate = true;
@@ -331,7 +355,7 @@ function finalizeCodeModeMetadata(
331
355
 
332
356
  /** Owns bounded CodeMode Session records and one isolated Deno process per live Session. */
333
357
  export class CodeModeSessionCoordinator {
334
- private readonly records = new Map<CodeModeSessionId, CodeModeSessionRecord>();
358
+ private readonly records = new Map<string, CodeModeSessionRecord>();
335
359
  private readonly runtime: CodeModeRuntime;
336
360
  private accessSequence = 0;
337
361
  private cellSequence = 0;
@@ -357,7 +381,7 @@ export class CodeModeSessionCoordinator {
357
381
  ): Promise<CodeModeSessionOperationResult> {
358
382
  if (this.shuttingDown) {
359
383
  const candidateSessionId = input.sessionId ?? this.runtime.createSessionId();
360
- const parsedSessionId = parseCodeModeSessionId(candidateSessionId);
384
+ const parsedSessionId = parsestring(candidateSessionId);
361
385
  if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
362
386
  return {
363
387
  result: createCodeModeFailure(
@@ -373,7 +397,7 @@ export class CodeModeSessionCoordinator {
373
397
  let located: LocateCodeModeSessionResult;
374
398
  if (input.sessionId === undefined) located = await this.createLiveSession();
375
399
  else {
376
- const parsedSessionId = parseCodeModeSessionId(input.sessionId);
400
+ const parsedSessionId = parsestring(input.sessionId);
377
401
  if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
378
402
  located = this.findSession(parsedSessionId.value);
379
403
  }
@@ -446,7 +470,7 @@ export class CodeModeSessionCoordinator {
446
470
 
447
471
  /** Polls the latest retained Cell result without consuming that public result. */
448
472
  result(sessionIdValue: string): CodeModeSessionOperationResult {
449
- const parsedSessionId = parseCodeModeSessionId(sessionIdValue);
473
+ const parsedSessionId = parsestring(sessionIdValue);
450
474
  if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
451
475
  const sessionId = parsedSessionId.value;
452
476
  const record = this.records.get(sessionId);
@@ -495,25 +519,33 @@ export class CodeModeSessionCoordinator {
495
519
 
496
520
  /** Return the shortest currently unique Session prefix, or the full unknown historical ID. */
497
521
  formatSessionPrefix(sessionIdValue: string): string {
498
- const parsed = parseCodeModeSessionId(sessionIdValue);
522
+ const parsed = parsestring(sessionIdValue);
499
523
  if (!parsed.ok || !this.records.has(parsed.value)) return sessionIdValue;
500
- const sessionIds = [...this.records.keys()];
501
- let length = Math.min(8, sessionIdValue.length);
502
- while (
503
- length < sessionIdValue.length &&
504
- sessionIds.some(
505
- (candidate) =>
506
- candidate !== parsed.value && candidate.startsWith(sessionIdValue.slice(0, length)),
507
- )
508
- ) {
509
- length += 1;
510
- }
511
- return sessionIdValue.slice(0, length);
524
+ const others = [...this.records.keys()].filter((candidate) => candidate !== parsed.value);
525
+ return shortestUniqueCodeModeSessionPrefix(sessionIdValue, others);
526
+ }
527
+
528
+ private closedCellPresentation(
529
+ cellState: CodeModePresentationSnapshot["cell_state"],
530
+ ): CodeModePresentationSnapshot {
531
+ return {
532
+ version: 1,
533
+ cell_state: cellState,
534
+ session_state: "closed",
535
+ elapsed_ms: 0,
536
+ active_tool_names: [],
537
+ active_tool_count: 0,
538
+ nested_tool_count: 0,
539
+ succeeded_nested_tool_count: 0,
540
+ failed_nested_tool_count: 0,
541
+ nested_tools: [],
542
+ omitted_nested_tool_count: 0,
543
+ };
512
544
  }
513
545
 
514
546
  /** Force-terminates one live CodeMode Session and retains its cancellation result. */
515
547
  async cancel(sessionIdValue: string): Promise<CodeModeSessionOperationResult> {
516
- const parsedSessionId = parseCodeModeSessionId(sessionIdValue);
548
+ const parsedSessionId = parsestring(sessionIdValue);
517
549
  if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
518
550
  const sessionId = parsedSessionId.value;
519
551
  const record = this.records.get(sessionId);
@@ -535,19 +567,7 @@ export class CodeModeSessionCoordinator {
535
567
  });
536
568
  } else {
537
569
  record.lastActivityAtMs = this.runtime.now();
538
- record.latestPresentation = {
539
- version: 1,
540
- cell_state: "cancelled",
541
- session_state: "closed",
542
- elapsed_ms: 0,
543
- active_tool_names: [],
544
- active_tool_count: 0,
545
- nested_tool_count: 0,
546
- succeeded_nested_tool_count: 0,
547
- failed_nested_tool_count: 0,
548
- nested_tools: [],
549
- omitted_nested_tool_count: 0,
550
- };
570
+ record.latestPresentation = this.closedCellPresentation("cancelled");
551
571
  this.replaceWithTerminal(
552
572
  record,
553
573
  createCodeModeFailure(sessionId, "cancellation", "CodeMode Session was cancelled"),
@@ -564,7 +584,7 @@ export class CodeModeSessionCoordinator {
564
584
  }
565
585
 
566
586
  /** Releases every live Deno process; repeated shutdown calls share one completion. */
567
- shutdown(_reason: string): Promise<void> {
587
+ shutdown(): Promise<void> {
568
588
  if (this.shutdownPromise !== undefined) return this.shutdownPromise;
569
589
  this.shuttingDown = true;
570
590
  this.shutdownPromise = this.shutdownAllSessions();
@@ -594,7 +614,7 @@ export class CodeModeSessionCoordinator {
594
614
  }
595
615
 
596
616
  private async createLiveSession(): Promise<LocateCodeModeSessionResult> {
597
- const parsedSessionId = parseCodeModeSessionId(this.runtime.createSessionId());
617
+ const parsedSessionId = parsestring(this.runtime.createSessionId());
598
618
  if (!parsedSessionId.ok) {
599
619
  return {
600
620
  failure: createCodeModeFailure(
@@ -618,7 +638,7 @@ export class CodeModeSessionCoordinator {
618
638
  const liveRecords = [...this.records.values()].filter(
619
639
  (record): record is LiveCodeModeSession => record.state === "live",
620
640
  );
621
- let reclaimedSessionId: CodeModeSessionId | undefined;
641
+ let reclaimedSessionId: string | undefined;
622
642
  if (liveRecords.length >= this.options.maxSessions) {
623
643
  const reclaimedRecord = liveRecords
624
644
  .filter((record) => record.currentCell === undefined)
@@ -698,7 +718,7 @@ export class CodeModeSessionCoordinator {
698
718
  return reclaimedSessionId === undefined ? { record } : { record, reclaimedSessionId };
699
719
  }
700
720
 
701
- private findSession(sessionId: CodeModeSessionId): LocateCodeModeSessionResult {
721
+ private findSession(sessionId: string): LocateCodeModeSessionResult {
702
722
  const record = this.records.get(sessionId);
703
723
  return record === undefined
704
724
  ? { failure: createCodeModeFailure(sessionId, "unknown", "Unknown CodeMode Session") }
@@ -794,10 +814,7 @@ export class CodeModeSessionCoordinator {
794
814
  }
795
815
  }
796
816
 
797
- private handleWorkerResponse(
798
- sessionId: CodeModeSessionId,
799
- response: CodeModeWorkerResponse,
800
- ): void {
817
+ private handleWorkerResponse(sessionId: string, response: CodeModeWorkerResponse): void {
801
818
  const record = this.records.get(sessionId);
802
819
  if (record?.state !== "live" || record.currentCell === undefined) return;
803
820
  const cell = record.currentCell;
@@ -810,7 +827,11 @@ export class CodeModeSessionCoordinator {
810
827
  return;
811
828
  }
812
829
  if (response.resultJson === undefined) {
813
- this.settleReusableCell(record, cell, createCodeModeSuccess(sessionId));
830
+ this.settleReusableCell(
831
+ record,
832
+ cell,
833
+ createCodeModeSuccess(sessionId, undefined, response.console),
834
+ );
814
835
  return;
815
836
  }
816
837
  const data = this.parseJsonString(response.resultJson, { allowUndefined: true });
@@ -818,11 +839,15 @@ export class CodeModeSessionCoordinator {
818
839
  this.settleReusableCell(
819
840
  record,
820
841
  cell,
821
- createCodeModeFailure(sessionId, "serialization", data.message),
842
+ createCodeModeFailure(sessionId, "serialization", data.message, response.console),
822
843
  );
823
844
  return;
824
845
  }
825
- this.settleReusableCell(record, cell, createCodeModeSuccess(sessionId, data.value));
846
+ this.settleReusableCell(
847
+ record,
848
+ cell,
849
+ createCodeModeSuccess(sessionId, data.value, response.console),
850
+ );
826
851
  return;
827
852
  }
828
853
  if (response.type === "cell-error") {
@@ -834,15 +859,25 @@ export class CodeModeSessionCoordinator {
834
859
  return;
835
860
  }
836
861
  if (response.error.code === "runtime") {
837
- this.fatalizeSession(record, cell, {
862
+ const failure = {
838
863
  code: response.error.code,
839
864
  message: response.error.message,
840
- });
865
+ } as const;
866
+ this.fatalizeSession(
867
+ record,
868
+ cell,
869
+ response.console === undefined ? failure : { ...failure, console: response.console },
870
+ );
841
871
  } else {
842
872
  this.settleReusableCell(
843
873
  record,
844
874
  cell,
845
- createCodeModeFailure(sessionId, response.error.code, response.error.message),
875
+ createCodeModeFailure(
876
+ sessionId,
877
+ response.error.code,
878
+ response.error.message,
879
+ response.console,
880
+ ),
846
881
  );
847
882
  }
848
883
  return;
@@ -1170,10 +1205,19 @@ export class CodeModeSessionCoordinator {
1170
1205
  ): void {
1171
1206
  if (!this.isCurrentCell(record, cell)) return;
1172
1207
  this.clearCellResources(cell);
1173
- const retainedResult =
1174
- result.result === "success" && cell.reclaimedSessionId !== undefined
1175
- ? { ...result, reclaimedSessionId: cell.reclaimedSessionId }
1176
- : result;
1208
+ let retainedResult = result;
1209
+ if (result.result === "success" && cell.reclaimedSessionId !== undefined) {
1210
+ if (result.console === undefined) {
1211
+ retainedResult = { ...result, reclaimedSessionId: cell.reclaimedSessionId };
1212
+ } else {
1213
+ const { console: consoleEntries, ...resultWithoutConsole } = result;
1214
+ retainedResult = {
1215
+ ...resultWithoutConsole,
1216
+ reclaimedSessionId: cell.reclaimedSessionId,
1217
+ console: consoleEntries,
1218
+ };
1219
+ }
1220
+ }
1177
1221
  const presentation = this.settledCellPresentation(cell, retainedResult, "live");
1178
1222
  const settledAtMs = this.runtime.now();
1179
1223
  record.latestResult = retainedResult;
@@ -1199,7 +1243,12 @@ export class CodeModeSessionCoordinator {
1199
1243
  if (failure.code === "termination") cell.metadata.terminate = true;
1200
1244
  cell.abortController.abort();
1201
1245
  this.clearCellResources(cell);
1202
- const result = createCodeModeFailure(record.sessionId, failure.code, failure.message);
1246
+ const result = createCodeModeFailure(
1247
+ record.sessionId,
1248
+ failure.code,
1249
+ failure.message,
1250
+ failure.console,
1251
+ );
1203
1252
  const settledAtMs = this.runtime.now();
1204
1253
  record.latestPresentation = this.cellPresentation(
1205
1254
  cell,
@@ -1247,26 +1296,14 @@ export class CodeModeSessionCoordinator {
1247
1296
  return stop;
1248
1297
  }
1249
1298
 
1250
- private handleWorkerFailure(sessionId: CodeModeSessionId, message: string): void {
1299
+ private handleWorkerFailure(sessionId: string, message: string): void {
1251
1300
  const record = this.records.get(sessionId);
1252
1301
  if (record?.state !== "live") return;
1253
1302
  if (record.currentCell !== undefined) {
1254
1303
  this.fatalizeSession(record, record.currentCell, { code: "runtime", message });
1255
1304
  } else {
1256
1305
  record.lastActivityAtMs = this.runtime.now();
1257
- record.latestPresentation = {
1258
- version: 1,
1259
- cell_state: "failed",
1260
- session_state: "closed",
1261
- elapsed_ms: 0,
1262
- active_tool_names: [],
1263
- active_tool_count: 0,
1264
- nested_tool_count: 0,
1265
- succeeded_nested_tool_count: 0,
1266
- failed_nested_tool_count: 0,
1267
- nested_tools: [],
1268
- omitted_nested_tool_count: 0,
1269
- };
1306
+ record.latestPresentation = this.closedCellPresentation("failed");
1270
1307
  this.replaceWithTerminal(record, createCodeModeFailure(sessionId, "runtime", message));
1271
1308
  try {
1272
1309
  this.options.onUnexpectedFailure?.(Object.freeze({ sessionId, message }));
@@ -1387,7 +1424,7 @@ export class CodeModeSessionCoordinator {
1387
1424
  }
1388
1425
  }
1389
1426
 
1390
- private retainTerminalFailure(sessionId: CodeModeSessionId, failure: CodeModeResult): void {
1427
+ private retainTerminalFailure(sessionId: string, failure: CodeModeResult): void {
1391
1428
  this.records.set(sessionId, {
1392
1429
  state: "terminal",
1393
1430
  sessionId,