@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 +48 -12
- package/package.json +1 -1
- package/src/codemode-console-output.ts +11 -0
- package/src/codemode-observer-ui.ts +15 -36
- package/src/codemode-session-coordinator.ts +127 -90
- package/src/codemode-tool-catalog.ts +4 -6
- package/src/codemode-tool-contract.ts +37 -7
- package/src/codemode-tool-rendering.ts +13 -7
- package/src/codemode-worker-protocol.ts +141 -33
- package/src/codemode-worker.ts +283 -18
- package/src/pi-codemode-extension.ts +17 -21
- package/src/pi-tool-bridge.ts +5 -38
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
|
|
127
|
-
rows show the full Session ID, explicit call arguments,
|
|
128
|
-
|
|
129
|
-
|
|
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,
|
|
259
|
-
frozen `Deno.version` identity. Raw
|
|
260
|
-
|
|
261
|
-
withheld.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
@@ -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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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?:
|
|
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?:
|
|
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:
|
|
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:
|
|
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?:
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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 =
|
|
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<
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
522
|
+
const parsed = parsestring(sessionIdValue);
|
|
499
523
|
if (!parsed.ok || !this.records.has(parsed.value)) return sessionIdValue;
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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:
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
:
|
|
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(
|
|
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:
|
|
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:
|
|
1427
|
+
private retainTerminalFailure(sessionId: string, failure: CodeModeResult): void {
|
|
1391
1428
|
this.records.set(sessionId, {
|
|
1392
1429
|
state: "terminal",
|
|
1393
1430
|
sessionId,
|