@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.
@@ -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;
@@ -86,9 +84,18 @@ export type CodeModeObserverSnapshot = {
86
84
  readonly sessions: readonly CodeModeObservedSession[];
87
85
  };
88
86
 
87
+ /** Read-only facts for one live CodeMode Session, ordered by admission priority. */
88
+ export type CodeModeListedSession = {
89
+ readonly sessionId: string;
90
+ readonly state: "running" | "idle";
91
+ readonly cellCount: number;
92
+ /** Parent wall-clock time of the latest execution-visible transition in Unix-epoch milliseconds. */
93
+ readonly lastActivityAtMs: number;
94
+ };
95
+
89
96
  /** One idle CodeMode worker failure not represented by an active Cell result. */
90
97
  export type CodeModeUnexpectedFailure = {
91
- readonly sessionId: CodeModeSessionId;
98
+ readonly sessionId: string;
92
99
  readonly message: string;
93
100
  };
94
101
 
@@ -113,7 +120,7 @@ export type CodeModeNestedToolUpdate = AgentToolResult<unknown>;
113
120
 
114
121
  /** One complete guest batch plus its Cell-scoped cancellation and update capabilities. */
115
122
  export type CodeModeNestedToolBatch = {
116
- readonly sessionId: CodeModeSessionId;
123
+ readonly sessionId: string;
117
124
  readonly batchId: string;
118
125
  readonly calls: readonly CodeModeNestedToolCall[];
119
126
  readonly signal: AbortSignal;
@@ -176,10 +183,16 @@ type MutableCodeModeOuterToolMetadata = {
176
183
  terminate?: boolean;
177
184
  };
178
185
 
186
+ type CreateActiveCodeModeCellOptions = {
187
+ onUpdate?: (update: CodeModeNestedToolUpdate) => void;
188
+ reclaimedSessionId?: string;
189
+ };
190
+
179
191
  type ActiveCodeModeCell = {
180
192
  readonly cellId: string;
181
193
  readonly ordinal: number;
182
194
  readonly startedAtMs: number;
195
+ readonly reclaimedSessionId?: string;
183
196
  readonly abortController: AbortController;
184
197
  readonly completion: Promise<CodeModeResult>;
185
198
  readonly resolveCompletion: (result: CodeModeResult) => void;
@@ -198,7 +211,7 @@ type ActiveCodeModeCell = {
198
211
 
199
212
  type LiveCodeModeSession = {
200
213
  readonly state: "live";
201
- readonly sessionId: CodeModeSessionId;
214
+ readonly sessionId: string;
202
215
  readonly worker: CodeModeWorkerProcess;
203
216
  lastAccess: number;
204
217
  lastActivityAtMs: number;
@@ -212,7 +225,7 @@ type LiveCodeModeSession = {
212
225
 
213
226
  type TerminalCodeModeSession = {
214
227
  readonly state: "terminal";
215
- readonly sessionId: CodeModeSessionId;
228
+ readonly sessionId: string;
216
229
  lastAccess: number;
217
230
  readonly lastActivityAtMs: number;
218
231
  readonly cellCount: number;
@@ -224,24 +237,53 @@ type TerminalCodeModeSession = {
224
237
 
225
238
  type CodeModeSessionRecord = LiveCodeModeSession | TerminalCodeModeSession;
226
239
 
227
- type LocateCodeModeSessionResult = {
228
- readonly record?: CodeModeSessionRecord;
229
- readonly failure: CodeModeResult;
230
- };
240
+ type LocateCodeModeSessionResult =
241
+ | {
242
+ readonly record: CodeModeSessionRecord;
243
+ readonly reclaimedSessionId?: string;
244
+ }
245
+ | { readonly failure: CodeModeResult };
231
246
 
232
247
  type FatalCodeModeSessionFailure = {
233
248
  readonly code: Extract<CodeModeErrorCode, "timeout" | "cancellation" | "termination" | "runtime">;
234
249
  readonly message: string;
250
+ readonly console?: readonly CodeModeConsoleEntry[];
235
251
  };
236
252
 
237
- type ParseCodeModeSessionIdResult =
238
- | { readonly ok: true; readonly value: CodeModeSessionId }
239
- | { 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
+ }
240
273
 
241
- function parseCodeModeSessionId(value: string): ParseCodeModeSessionIdResult {
242
- if (value.length === 0) return { ok: false };
243
- // SAFETY: This parser establishes the only CodeMode Session ID invariant (non-empty) before applying the domain brand.
244
- 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));
245
287
  }
246
288
 
247
289
  function invalidCodeModeSessionResult(): CodeModeSessionOperationResult {
@@ -258,12 +300,11 @@ function emptyMetadataAccumulator(): CodeModeMetadataAccumulator {
258
300
  return { addedToolNames: new Set(), terminate: false };
259
301
  }
260
302
 
261
- 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");
262
306
  if (left === undefined) {
263
- return {
264
- ...right,
265
- cost: { ...right.cost },
266
- };
307
+ return { ...right, cost: { ...right.cost } };
267
308
  }
268
309
  const combined: Usage = {
269
310
  input: left.input + right.input,
@@ -293,7 +334,7 @@ function mergeCodeModeOuterMetadata(
293
334
  metadata: CodeModeOuterToolMetadata,
294
335
  ): void {
295
336
  if (metadata.usage !== undefined) {
296
- accumulator.usage = combineCodeModeUsage(accumulator.usage, metadata.usage);
337
+ accumulator.usage = addCodeModeUsage(accumulator.usage, metadata.usage);
297
338
  }
298
339
  for (const name of metadata.addedToolNames ?? []) accumulator.addedToolNames.add(name);
299
340
  if (metadata.terminate === true) accumulator.terminate = true;
@@ -314,7 +355,7 @@ function finalizeCodeModeMetadata(
314
355
 
315
356
  /** Owns bounded CodeMode Session records and one isolated Deno process per live Session. */
316
357
  export class CodeModeSessionCoordinator {
317
- private readonly records = new Map<CodeModeSessionId, CodeModeSessionRecord>();
358
+ private readonly records = new Map<string, CodeModeSessionRecord>();
318
359
  private readonly runtime: CodeModeRuntime;
319
360
  private accessSequence = 0;
320
361
  private cellSequence = 0;
@@ -325,6 +366,7 @@ export class CodeModeSessionCoordinator {
325
366
  (update: CodeModeNestedToolUpdate) => void
326
367
  >();
327
368
  private readonly pendingProcessStops = new Set<Promise<void>>();
369
+ private admissionQueue: Promise<void> = Promise.resolve();
328
370
 
329
371
  /** Creates one coordinator from its Pi bridge, limits, and parent runtime capabilities. */
330
372
  constructor(private readonly options: CodeModeSessionCoordinatorOptions) {
@@ -339,7 +381,7 @@ export class CodeModeSessionCoordinator {
339
381
  ): Promise<CodeModeSessionOperationResult> {
340
382
  if (this.shuttingDown) {
341
383
  const candidateSessionId = input.sessionId ?? this.runtime.createSessionId();
342
- const parsedSessionId = parseCodeModeSessionId(candidateSessionId);
384
+ const parsedSessionId = parsestring(candidateSessionId);
343
385
  if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
344
386
  return {
345
387
  result: createCodeModeFailure(
@@ -349,76 +391,86 @@ export class CodeModeSessionCoordinator {
349
391
  ),
350
392
  };
351
393
  }
352
- let located: LocateCodeModeSessionResult;
353
- if (input.sessionId === undefined) located = this.createLiveSession();
354
- else {
355
- const parsedSessionId = parseCodeModeSessionId(input.sessionId);
356
- if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
357
- located = this.findSession(parsedSessionId.value);
358
- }
359
- if (located.record === undefined) return { result: located.failure };
360
- const record = located.record;
361
- this.touch(record);
362
- if (record.state === "terminal")
363
- return this.operationResult(
364
- record.latestResult,
365
- this.takeMetadata(record),
366
- record.latestPresentation,
367
- );
368
- if (record.currentCell !== undefined) {
369
- return {
370
- result: createCodeModeFailure(
371
- record.sessionId,
372
- "busy",
373
- "CodeMode Session already has an active Cell",
374
- ),
375
- };
376
- }
394
+ let releaseAdmission =
395
+ input.sessionId === undefined ? await this.acquireSessionAdmission() : undefined;
396
+ try {
397
+ let located: LocateCodeModeSessionResult;
398
+ if (input.sessionId === undefined) located = await this.createLiveSession();
399
+ else {
400
+ const parsedSessionId = parsestring(input.sessionId);
401
+ if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
402
+ located = this.findSession(parsedSessionId.value);
403
+ }
404
+ if ("failure" in located) return { result: located.failure };
405
+ const record = located.record;
406
+ this.touch(record);
407
+ if (record.state === "terminal")
408
+ return this.operationResult(
409
+ record.latestResult,
410
+ this.takeMetadata(record),
411
+ record.latestPresentation,
412
+ );
413
+ if (record.currentCell !== undefined) {
414
+ return {
415
+ result: createCodeModeFailure(
416
+ record.sessionId,
417
+ "busy",
418
+ "CodeMode Session already has an active Cell",
419
+ ),
420
+ };
421
+ }
377
422
 
378
- const shouldWait = input.wait !== false;
379
- const cell = this.createActiveCell(
380
- record,
381
- shouldWait && onUpdate !== undefined ? { onUpdate } : {},
382
- );
383
- const priorMetadata = this.takeMetadata(record);
384
- if (priorMetadata !== undefined) mergeCodeModeOuterMetadata(cell.metadata, priorMetadata);
385
- record.currentCell = cell;
386
- record.latestResult = createCodeModePending(record.sessionId);
387
- record.lastActivityAtMs = cell.startedAtMs;
388
- this.publishObserverSnapshot();
389
- this.emitCellProgress(record, cell);
390
- this.scheduleCellProgress(record, cell);
391
- void this.startCell(record, cell, input);
423
+ const shouldWait = input.wait !== false;
424
+ const cellOptions: CreateActiveCodeModeCellOptions = {};
425
+ if (shouldWait && onUpdate !== undefined) cellOptions.onUpdate = onUpdate;
426
+ if (located.reclaimedSessionId !== undefined) {
427
+ cellOptions.reclaimedSessionId = located.reclaimedSessionId;
428
+ }
429
+ const cell = this.createActiveCell(record, cellOptions);
430
+ const priorMetadata = this.takeMetadata(record);
431
+ if (priorMetadata !== undefined) mergeCodeModeOuterMetadata(cell.metadata, priorMetadata);
432
+ record.currentCell = cell;
433
+ record.latestResult = createCodeModePending(record.sessionId);
434
+ record.lastActivityAtMs = cell.startedAtMs;
435
+ this.publishObserverSnapshot();
436
+ this.emitCellProgress(record, cell);
437
+ this.scheduleCellProgress(record, cell);
438
+ void this.startCell(record, cell, input);
439
+ releaseAdmission?.();
440
+ releaseAdmission = undefined;
392
441
 
393
- const pending = createCodeModePending(record.sessionId);
394
- if (!shouldWait) {
395
- return this.operationResult(pending, undefined, this.runningCellPresentation(cell));
396
- }
397
- const abort = (): void => {
398
- this.fatalizeSession(record, cell, {
399
- code: "cancellation",
400
- message: "CodeMode Cell was cancelled",
401
- });
402
- };
403
- if (signal?.aborted === true) abort();
404
- else signal?.addEventListener("abort", abort, { once: true });
405
- try {
406
- const result = await cell.completion;
407
- const retainedRecord = this.records.get(record.sessionId) ?? record;
408
- return this.operationResult(
409
- result,
410
- this.takeMetadata(retainedRecord),
411
- retainedRecord.latestPresentation,
412
- );
442
+ const pending = createCodeModePending(record.sessionId);
443
+ if (!shouldWait) {
444
+ return this.operationResult(pending, undefined, this.runningCellPresentation(cell));
445
+ }
446
+ const abort = (): void => {
447
+ this.fatalizeSession(record, cell, {
448
+ code: "cancellation",
449
+ message: "CodeMode Cell was cancelled",
450
+ });
451
+ };
452
+ if (signal?.aborted === true) abort();
453
+ else signal?.addEventListener("abort", abort, { once: true });
454
+ try {
455
+ const result = await cell.completion;
456
+ const retainedRecord = this.records.get(record.sessionId) ?? record;
457
+ return this.operationResult(
458
+ result,
459
+ this.takeMetadata(retainedRecord),
460
+ retainedRecord.latestPresentation,
461
+ );
462
+ } finally {
463
+ cell.acceptsUpdates = false;
464
+ signal?.removeEventListener("abort", abort);
465
+ }
413
466
  } finally {
414
- cell.acceptsUpdates = false;
415
- signal?.removeEventListener("abort", abort);
467
+ releaseAdmission?.();
416
468
  }
417
469
  }
418
470
 
419
471
  /** Polls the latest retained Cell result without consuming that public result. */
420
472
  result(sessionIdValue: string): CodeModeSessionOperationResult {
421
- const parsedSessionId = parseCodeModeSessionId(sessionIdValue);
473
+ const parsedSessionId = parsestring(sessionIdValue);
422
474
  if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
423
475
  const sessionId = parsedSessionId.value;
424
476
  const record = this.records.get(sessionId);
@@ -437,6 +489,26 @@ export class CodeModeSessionCoordinator {
437
489
  return this.operationResult(result, this.takeMetadata(record), record.latestPresentation);
438
490
  }
439
491
 
492
+ /** Lists all live Sessions with idle least-recently-used entries before running entries. */
493
+ listSessions(): readonly CodeModeListedSession[] {
494
+ const sessions = [...this.records.values()]
495
+ .filter((record): record is LiveCodeModeSession => record.state === "live")
496
+ .toSorted((left, right) => {
497
+ const stateOrder =
498
+ Number(left.currentCell !== undefined) - Number(right.currentCell !== undefined);
499
+ return stateOrder === 0 ? left.lastAccess - right.lastAccess : stateOrder;
500
+ })
501
+ .map((record) =>
502
+ Object.freeze({
503
+ sessionId: record.sessionId,
504
+ state: record.currentCell === undefined ? "idle" : "running",
505
+ cellCount: record.cellCount,
506
+ lastActivityAtMs: record.lastActivityAtMs,
507
+ } satisfies CodeModeListedSession),
508
+ );
509
+ return Object.freeze(sessions);
510
+ }
511
+
440
512
  /** Returns immutable, non-authoritative facts for the ephemeral CodeMode Observer UI. */
441
513
  inspectObserverSnapshot(): CodeModeObserverSnapshot {
442
514
  const sessions = [...this.records.values()]
@@ -447,25 +519,33 @@ export class CodeModeSessionCoordinator {
447
519
 
448
520
  /** Return the shortest currently unique Session prefix, or the full unknown historical ID. */
449
521
  formatSessionPrefix(sessionIdValue: string): string {
450
- const parsed = parseCodeModeSessionId(sessionIdValue);
522
+ const parsed = parsestring(sessionIdValue);
451
523
  if (!parsed.ok || !this.records.has(parsed.value)) return sessionIdValue;
452
- const sessionIds = [...this.records.keys()];
453
- let length = Math.min(8, sessionIdValue.length);
454
- while (
455
- length < sessionIdValue.length &&
456
- sessionIds.some(
457
- (candidate) =>
458
- candidate !== parsed.value && candidate.startsWith(sessionIdValue.slice(0, length)),
459
- )
460
- ) {
461
- length += 1;
462
- }
463
- 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
+ };
464
544
  }
465
545
 
466
546
  /** Force-terminates one live CodeMode Session and retains its cancellation result. */
467
547
  async cancel(sessionIdValue: string): Promise<CodeModeSessionOperationResult> {
468
- const parsedSessionId = parseCodeModeSessionId(sessionIdValue);
548
+ const parsedSessionId = parsestring(sessionIdValue);
469
549
  if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
470
550
  const sessionId = parsedSessionId.value;
471
551
  const record = this.records.get(sessionId);
@@ -487,19 +567,7 @@ export class CodeModeSessionCoordinator {
487
567
  });
488
568
  } else {
489
569
  record.lastActivityAtMs = this.runtime.now();
490
- record.latestPresentation = {
491
- version: 1,
492
- cell_state: "cancelled",
493
- session_state: "closed",
494
- elapsed_ms: 0,
495
- active_tool_names: [],
496
- active_tool_count: 0,
497
- nested_tool_count: 0,
498
- succeeded_nested_tool_count: 0,
499
- failed_nested_tool_count: 0,
500
- nested_tools: [],
501
- omitted_nested_tool_count: 0,
502
- };
570
+ record.latestPresentation = this.closedCellPresentation("cancelled");
503
571
  this.replaceWithTerminal(
504
572
  record,
505
573
  createCodeModeFailure(sessionId, "cancellation", "CodeMode Session was cancelled"),
@@ -516,7 +584,7 @@ export class CodeModeSessionCoordinator {
516
584
  }
517
585
 
518
586
  /** Releases every live Deno process; repeated shutdown calls share one completion. */
519
- shutdown(_reason: string): Promise<void> {
587
+ shutdown(): Promise<void> {
520
588
  if (this.shutdownPromise !== undefined) return this.shutdownPromise;
521
589
  this.shuttingDown = true;
522
590
  this.shutdownPromise = this.shutdownAllSessions();
@@ -537,8 +605,16 @@ export class CodeModeSessionCoordinator {
537
605
  await Promise.all(this.pendingProcessStops);
538
606
  }
539
607
 
540
- private createLiveSession(): LocateCodeModeSessionResult {
541
- const parsedSessionId = parseCodeModeSessionId(this.runtime.createSessionId());
608
+ private async acquireSessionAdmission(): Promise<() => void> {
609
+ const previousAdmission = this.admissionQueue;
610
+ const admission = Promise.withResolvers<void>();
611
+ this.admissionQueue = previousAdmission.then(() => admission.promise);
612
+ await previousAdmission;
613
+ return admission.resolve;
614
+ }
615
+
616
+ private async createLiveSession(): Promise<LocateCodeModeSessionResult> {
617
+ const parsedSessionId = parsestring(this.runtime.createSessionId());
542
618
  if (!parsedSessionId.ok) {
543
619
  return {
544
620
  failure: createCodeModeFailure(
@@ -549,13 +625,67 @@ export class CodeModeSessionCoordinator {
549
625
  };
550
626
  }
551
627
  const sessionId = parsedSessionId.value;
628
+ if (this.shuttingDown) {
629
+ const failure = createCodeModeFailure(
630
+ sessionId,
631
+ "runtime",
632
+ "CodeMode coordinator is shutting down",
633
+ );
634
+ this.retainTerminalFailure(sessionId, failure);
635
+ return { failure };
636
+ }
552
637
  this.evictTerminalRecords();
553
- const liveCount = [...this.records.values()].filter((record) => record.state === "live").length;
554
- if (liveCount >= this.options.maxSessions) {
638
+ const liveRecords = [...this.records.values()].filter(
639
+ (record): record is LiveCodeModeSession => record.state === "live",
640
+ );
641
+ let reclaimedSessionId: string | undefined;
642
+ if (liveRecords.length >= this.options.maxSessions) {
643
+ const reclaimedRecord = liveRecords
644
+ .filter((record) => record.currentCell === undefined)
645
+ .toSorted((left, right) => left.lastAccess - right.lastAccess)[0];
646
+ if (reclaimedRecord === undefined) {
647
+ const failure = createCodeModeFailure(
648
+ sessionId,
649
+ "capacity",
650
+ "CodeMode Session capacity is exhausted",
651
+ );
652
+ this.retainTerminalFailure(sessionId, failure);
653
+ return { failure };
654
+ }
655
+ reclaimedSessionId = reclaimedRecord.sessionId;
656
+ reclaimedRecord.lastActivityAtMs = this.runtime.now();
657
+ this.touch(reclaimedRecord);
658
+ if (reclaimedRecord.latestPresentation !== undefined) {
659
+ reclaimedRecord.latestPresentation = {
660
+ ...reclaimedRecord.latestPresentation,
661
+ session_state: "closed",
662
+ };
663
+ }
664
+ this.replaceWithTerminal(
665
+ reclaimedRecord,
666
+ createCodeModeFailure(
667
+ reclaimedRecord.sessionId,
668
+ "eviction",
669
+ "CodeMode Session was reclaimed to free capacity.",
670
+ ),
671
+ );
672
+ try {
673
+ await this.stopWorker(reclaimedRecord.worker, "shutdown");
674
+ } catch (cause) {
675
+ const message =
676
+ cause instanceof Error
677
+ ? `CodeMode Session reclamation failed: ${cause.message}`
678
+ : "CodeMode Session reclamation failed: Deno process did not stop cleanly";
679
+ const failure = createCodeModeFailure(sessionId, "runtime", message);
680
+ this.retainTerminalFailure(sessionId, failure);
681
+ return { failure };
682
+ }
683
+ }
684
+ if (this.shuttingDown) {
555
685
  const failure = createCodeModeFailure(
556
686
  sessionId,
557
- "capacity",
558
- "CodeMode Session capacity is exhausted",
687
+ "runtime",
688
+ "CodeMode coordinator is shutting down",
559
689
  );
560
690
  this.retainTerminalFailure(sessionId, failure);
561
691
  return { failure };
@@ -585,22 +715,22 @@ export class CodeModeSessionCoordinator {
585
715
  cellCount: 0,
586
716
  };
587
717
  this.records.set(sessionId, record);
588
- return { record, failure: createCodeModePending(sessionId) };
718
+ return reclaimedSessionId === undefined ? { record } : { record, reclaimedSessionId };
589
719
  }
590
720
 
591
- private findSession(sessionId: CodeModeSessionId): LocateCodeModeSessionResult {
721
+ private findSession(sessionId: string): LocateCodeModeSessionResult {
592
722
  const record = this.records.get(sessionId);
593
723
  return record === undefined
594
724
  ? { failure: createCodeModeFailure(sessionId, "unknown", "Unknown CodeMode Session") }
595
- : { record, failure: createCodeModePending(sessionId) };
725
+ : { record };
596
726
  }
597
727
 
598
728
  private createActiveCell(
599
729
  record: LiveCodeModeSession,
600
- options: { readonly onUpdate?: (update: CodeModeNestedToolUpdate) => void },
730
+ options: CreateActiveCodeModeCellOptions,
601
731
  ): ActiveCodeModeCell {
602
732
  const completion = Promise.withResolvers<CodeModeResult>();
603
- const cell: ActiveCodeModeCell = {
733
+ const cellBase: Omit<ActiveCodeModeCell, "reclaimedSessionId"> = {
604
734
  cellId: `cell-${++this.cellSequence}`,
605
735
  ordinal: ++record.cellCount,
606
736
  startedAtMs: this.runtime.now(),
@@ -617,6 +747,10 @@ export class CodeModeSessionCoordinator {
617
747
  acceptsUpdates: options.onUpdate !== undefined,
618
748
  settled: false,
619
749
  };
750
+ const cell: ActiveCodeModeCell =
751
+ options.reclaimedSessionId === undefined
752
+ ? cellBase
753
+ : { ...cellBase, reclaimedSessionId: options.reclaimedSessionId };
620
754
  if (options.onUpdate !== undefined) this.activeUpdateCallbacks.set(cell, options.onUpdate);
621
755
  return cell;
622
756
  }
@@ -680,10 +814,7 @@ export class CodeModeSessionCoordinator {
680
814
  }
681
815
  }
682
816
 
683
- private handleWorkerResponse(
684
- sessionId: CodeModeSessionId,
685
- response: CodeModeWorkerResponse,
686
- ): void {
817
+ private handleWorkerResponse(sessionId: string, response: CodeModeWorkerResponse): void {
687
818
  const record = this.records.get(sessionId);
688
819
  if (record?.state !== "live" || record.currentCell === undefined) return;
689
820
  const cell = record.currentCell;
@@ -696,7 +827,11 @@ export class CodeModeSessionCoordinator {
696
827
  return;
697
828
  }
698
829
  if (response.resultJson === undefined) {
699
- this.settleReusableCell(record, cell, createCodeModeSuccess(sessionId));
830
+ this.settleReusableCell(
831
+ record,
832
+ cell,
833
+ createCodeModeSuccess(sessionId, undefined, response.console),
834
+ );
700
835
  return;
701
836
  }
702
837
  const data = this.parseJsonString(response.resultJson, { allowUndefined: true });
@@ -704,11 +839,15 @@ export class CodeModeSessionCoordinator {
704
839
  this.settleReusableCell(
705
840
  record,
706
841
  cell,
707
- createCodeModeFailure(sessionId, "serialization", data.message),
842
+ createCodeModeFailure(sessionId, "serialization", data.message, response.console),
708
843
  );
709
844
  return;
710
845
  }
711
- this.settleReusableCell(record, cell, createCodeModeSuccess(sessionId, data.value));
846
+ this.settleReusableCell(
847
+ record,
848
+ cell,
849
+ createCodeModeSuccess(sessionId, data.value, response.console),
850
+ );
712
851
  return;
713
852
  }
714
853
  if (response.type === "cell-error") {
@@ -720,15 +859,25 @@ export class CodeModeSessionCoordinator {
720
859
  return;
721
860
  }
722
861
  if (response.error.code === "runtime") {
723
- this.fatalizeSession(record, cell, {
862
+ const failure = {
724
863
  code: response.error.code,
725
864
  message: response.error.message,
726
- });
865
+ } as const;
866
+ this.fatalizeSession(
867
+ record,
868
+ cell,
869
+ response.console === undefined ? failure : { ...failure, console: response.console },
870
+ );
727
871
  } else {
728
872
  this.settleReusableCell(
729
873
  record,
730
874
  cell,
731
- createCodeModeFailure(sessionId, response.error.code, response.error.message),
875
+ createCodeModeFailure(
876
+ sessionId,
877
+ response.error.code,
878
+ response.error.message,
879
+ response.console,
880
+ ),
732
881
  );
733
882
  }
734
883
  return;
@@ -1056,18 +1205,31 @@ export class CodeModeSessionCoordinator {
1056
1205
  ): void {
1057
1206
  if (!this.isCurrentCell(record, cell)) return;
1058
1207
  this.clearCellResources(cell);
1059
- const presentation = this.settledCellPresentation(cell, result, "live");
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
+ }
1221
+ const presentation = this.settledCellPresentation(cell, retainedResult, "live");
1060
1222
  const settledAtMs = this.runtime.now();
1061
- record.latestResult = result;
1223
+ record.latestResult = retainedResult;
1062
1224
  record.latestPresentation = presentation;
1063
- record.lastCell = this.observeSettledCell(cell, result, settledAtMs);
1225
+ record.lastCell = this.observeSettledCell(cell, retainedResult, settledAtMs);
1064
1226
  record.lastActivityAtMs = settledAtMs;
1065
1227
  const metadata = finalizeCodeModeMetadata(cell.metadata);
1066
1228
  if (metadata === undefined) delete record.availableMetadata;
1067
1229
  else record.availableMetadata = metadata;
1068
1230
  delete record.currentCell;
1069
1231
  cell.settled = true;
1070
- cell.resolveCompletion(result);
1232
+ cell.resolveCompletion(retainedResult);
1071
1233
  this.touch(record);
1072
1234
  this.publishObserverSnapshot();
1073
1235
  }
@@ -1081,7 +1243,12 @@ export class CodeModeSessionCoordinator {
1081
1243
  if (failure.code === "termination") cell.metadata.terminate = true;
1082
1244
  cell.abortController.abort();
1083
1245
  this.clearCellResources(cell);
1084
- 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
+ );
1085
1252
  const settledAtMs = this.runtime.now();
1086
1253
  record.latestPresentation = this.cellPresentation(
1087
1254
  cell,
@@ -1129,26 +1296,14 @@ export class CodeModeSessionCoordinator {
1129
1296
  return stop;
1130
1297
  }
1131
1298
 
1132
- private handleWorkerFailure(sessionId: CodeModeSessionId, message: string): void {
1299
+ private handleWorkerFailure(sessionId: string, message: string): void {
1133
1300
  const record = this.records.get(sessionId);
1134
1301
  if (record?.state !== "live") return;
1135
1302
  if (record.currentCell !== undefined) {
1136
1303
  this.fatalizeSession(record, record.currentCell, { code: "runtime", message });
1137
1304
  } else {
1138
1305
  record.lastActivityAtMs = this.runtime.now();
1139
- record.latestPresentation = {
1140
- version: 1,
1141
- cell_state: "failed",
1142
- session_state: "closed",
1143
- elapsed_ms: 0,
1144
- active_tool_names: [],
1145
- active_tool_count: 0,
1146
- nested_tool_count: 0,
1147
- succeeded_nested_tool_count: 0,
1148
- failed_nested_tool_count: 0,
1149
- nested_tools: [],
1150
- omitted_nested_tool_count: 0,
1151
- };
1306
+ record.latestPresentation = this.closedCellPresentation("failed");
1152
1307
  this.replaceWithTerminal(record, createCodeModeFailure(sessionId, "runtime", message));
1153
1308
  try {
1154
1309
  this.options.onUnexpectedFailure?.(Object.freeze({ sessionId, message }));
@@ -1269,7 +1424,7 @@ export class CodeModeSessionCoordinator {
1269
1424
  }
1270
1425
  }
1271
1426
 
1272
- private retainTerminalFailure(sessionId: CodeModeSessionId, failure: CodeModeResult): void {
1427
+ private retainTerminalFailure(sessionId: string, failure: CodeModeResult): void {
1273
1428
  this.records.set(sessionId, {
1274
1429
  state: "terminal",
1275
1430
  sessionId,