@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 +96 -24
- package/package.json +6 -6
- package/src/codemode-console-output.ts +11 -0
- package/src/codemode-observer-ui.ts +19 -38
- package/src/codemode-session-coordinator.ts +313 -158
- package/src/codemode-tool-catalog.ts +4 -6
- package/src/codemode-tool-contract.ts +108 -14
- package/src/codemode-tool-rendering.ts +138 -39
- package/src/codemode-worker-protocol.ts +141 -33
- package/src/codemode-worker.ts +283 -18
- package/src/pi-codemode-extension.ts +31 -28
- package/src/pi-tool-bridge.ts +5 -38
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
229
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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 =
|
|
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<
|
|
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 =
|
|
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
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
522
|
+
const parsed = parsestring(sessionIdValue);
|
|
451
523
|
if (!parsed.ok || !this.records.has(parsed.value)) return sessionIdValue;
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
541
|
-
const
|
|
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
|
|
554
|
-
|
|
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
|
-
"
|
|
558
|
-
"CodeMode
|
|
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
|
|
718
|
+
return reclaimedSessionId === undefined ? { record } : { record, reclaimedSessionId };
|
|
589
719
|
}
|
|
590
720
|
|
|
591
|
-
private findSession(sessionId:
|
|
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
|
|
725
|
+
: { record };
|
|
596
726
|
}
|
|
597
727
|
|
|
598
728
|
private createActiveCell(
|
|
599
729
|
record: LiveCodeModeSession,
|
|
600
|
-
options:
|
|
730
|
+
options: CreateActiveCodeModeCellOptions,
|
|
601
731
|
): ActiveCodeModeCell {
|
|
602
732
|
const completion = Promise.withResolvers<CodeModeResult>();
|
|
603
|
-
const
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
1223
|
+
record.latestResult = retainedResult;
|
|
1062
1224
|
record.latestPresentation = presentation;
|
|
1063
|
-
record.lastCell = this.observeSettledCell(cell,
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
1427
|
+
private retainTerminalFailure(sessionId: string, failure: CodeModeResult): void {
|
|
1273
1428
|
this.records.set(sessionId, {
|
|
1274
1429
|
state: "terminal",
|
|
1275
1430
|
sessionId,
|