@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.
@@ -1,3 +1,4 @@
1
+ import { CODEMODE_CONSOLE_METHODS, type CodeModeConsoleEntry } from "./codemode-console-output.ts";
1
2
  import {
2
3
  CODEMODE_WORKER_MESSAGE_LIMIT_BYTES,
3
4
  parseCodeModeWorkerRequest,
@@ -9,13 +10,27 @@ import {
9
10
  } from "./codemode-worker-protocol.ts";
10
11
 
11
12
  const CODEMODE_WORKER_READ_BUFFER_BYTES = 64 * 1024;
13
+ const CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES = 512;
12
14
 
13
15
  type DenoByteReader = { read(buffer: Uint8Array): Promise<number | null> };
14
16
  type DenoByteWriter = { write(buffer: Uint8Array): Promise<number> };
17
+ type DenoInspectOptions = {
18
+ readonly colors: false;
19
+ readonly getters: false;
20
+ readonly customInspect: false;
21
+ };
15
22
  type CodeModeDenoNamespace = {
16
23
  readonly args: readonly string[];
17
24
  readonly stdin: DenoByteReader;
18
25
  readonly stdout: DenoByteWriter;
26
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Deno.inspect is the captured hostile-value formatter; coordinator tests cover getters, coercion hooks, custom inspectors, and Proxies.
27
+ readonly inspect: (value: unknown, options: DenoInspectOptions) => string;
28
+ readonly internal: symbol;
29
+ readonly [key: symbol]:
30
+ | {
31
+ readonly inspectArgs: (args: readonly unknown[], options: DenoInspectOptions) => string;
32
+ }
33
+ | undefined;
19
34
  readonly version: {
20
35
  readonly deno: string;
21
36
  readonly v8: string;
@@ -26,6 +41,11 @@ type CodeModeDenoNamespace = {
26
41
  declare const Deno: CodeModeDenoNamespace;
27
42
 
28
43
  const denoProcess = Deno;
44
+ const denoInspect = denoProcess.inspect;
45
+ const denoInternal = denoProcess[denoProcess.internal];
46
+ if (denoInternal === undefined)
47
+ throw new Error("Pi CodeMode: Deno Console formatter is unavailable");
48
+ const denoInspectArgs = denoInternal.inspectArgs;
29
49
  const arrayIsArray = Array.isArray;
30
50
  const arrayPrototype = Array.prototype;
31
51
  const blobConstructor = Blob;
@@ -43,6 +63,11 @@ const numberFrom = Number;
43
63
  const numberIsFinite = Number.isFinite;
44
64
  const numberIsSafeInteger = Number.isSafeInteger;
45
65
  const objectFreeze = Object.freeze;
66
+ const SAFE_DENO_INSPECT_OPTIONS = objectFreeze({
67
+ colors: false,
68
+ getters: false,
69
+ customInspect: false,
70
+ } as const);
46
71
  const objectPrototype = Object.prototype;
47
72
  const ownKeys = Reflect.ownKeys;
48
73
  const queueRuntimeMicrotask = queueMicrotask.bind(globalThis);
@@ -159,6 +184,9 @@ type ActiveWorkerCell = {
159
184
  readonly sessionId: string;
160
185
  readonly cellId: string;
161
186
  readonly pendingCalls: PendingGuestToolCall[];
187
+ readonly consoleEntries: CodeModeConsoleEntry[];
188
+ consoleBytes: number;
189
+ consoleOverflow: boolean;
162
190
  batchSequence: number;
163
191
  callSequence: number;
164
192
  batchScheduled: boolean;
@@ -206,6 +234,113 @@ function isReservedNotebookBindingName(name: string): boolean {
206
234
  return false;
207
235
  }
208
236
 
237
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Cell Console calls accept arbitrary guest values; captured Deno.inspect disables getters and custom inspection, while coordinator hostile-value tests cover coercion hooks and Proxies.
238
+ function inspectGuestConsoleValue(value: unknown): string {
239
+ try {
240
+ return denoInspect(value, SAFE_DENO_INSPECT_OPTIONS);
241
+ } catch {
242
+ return "[Uninspectable value]";
243
+ }
244
+ }
245
+
246
+ function formatGuestConsoleArguments(args: readonly unknown[]): string | undefined {
247
+ if (args.length === 0) return "";
248
+ const first = args[0];
249
+ const maximumTextLength =
250
+ CODEMODE_WORKER_MESSAGE_LIMIT_BYTES - CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES;
251
+ if (isGuestString(first) && first.length >= maximumTextLength) return undefined;
252
+ if (args.length === 1 && isGuestString(first)) return first;
253
+ if (isGuestString(first) && !first.includes("%")) {
254
+ let text = first;
255
+ for (let index = 1; index < args.length; index += 1) {
256
+ const value = args[index];
257
+ const rendered = isGuestString(value) ? value : inspectGuestConsoleValue(value);
258
+ if (text.length + rendered.length + 1 >= maximumTextLength) return undefined;
259
+ text += ` ${rendered}`;
260
+ }
261
+ return text;
262
+ }
263
+ if (!isGuestString(first)) {
264
+ let text = "";
265
+ for (let index = 0; index < args.length; index += 1) {
266
+ const value = args[index];
267
+ const rendered = isGuestString(value) ? value : inspectGuestConsoleValue(value);
268
+ const separatorLength = index === 0 ? 0 : 1;
269
+ if (text.length + rendered.length + separatorLength >= maximumTextLength) return undefined;
270
+ if (separatorLength > 0) text += " ";
271
+ text += rendered;
272
+ }
273
+ return text;
274
+ }
275
+
276
+ const safeArgs: unknown[] = [first];
277
+ for (let index = 1; index < args.length; index += 1) safeArgs[index] = args[index];
278
+ let format = "";
279
+ let argumentIndex = 1;
280
+ for (let index = 0; index < first.length; index += 1) {
281
+ const character = first[index];
282
+ if (character !== "%" || index + 1 >= first.length) {
283
+ format += character;
284
+ continue;
285
+ }
286
+ const token = first[index + 1];
287
+ if (token === "%") {
288
+ format += "%%";
289
+ index += 1;
290
+ continue;
291
+ }
292
+ if (
293
+ token !== "s" &&
294
+ token !== "d" &&
295
+ token !== "i" &&
296
+ token !== "f" &&
297
+ token !== "j" &&
298
+ token !== "o" &&
299
+ token !== "O" &&
300
+ token !== "c"
301
+ ) {
302
+ format += `%${token}`;
303
+ index += 1;
304
+ continue;
305
+ }
306
+ const value = safeArgs[argumentIndex];
307
+ if (value !== undefined || argumentIndex < safeArgs.length) {
308
+ if (isGuestReference(value)) {
309
+ const inspected = inspectGuestConsoleValue(value);
310
+ if (inspected.length >= maximumTextLength) return undefined;
311
+ safeArgs[argumentIndex] = inspected;
312
+ format += token === "c" ? "%c" : "%s";
313
+ } else {
314
+ format += `%${token}`;
315
+ }
316
+ argumentIndex += 1;
317
+ } else {
318
+ format += `%${token}`;
319
+ }
320
+ index += 1;
321
+ }
322
+ safeArgs[0] = format;
323
+ for (let index = argumentIndex; index < safeArgs.length; index += 1) {
324
+ const value = safeArgs[index];
325
+ if (isGuestReference(value)) {
326
+ const inspected = inspectGuestConsoleValue(value);
327
+ if (inspected.length >= maximumTextLength) return undefined;
328
+ safeArgs[index] = inspected;
329
+ }
330
+ }
331
+ let minimumFormattedBytes = 0;
332
+ for (const value of safeArgs) {
333
+ if (isGuestString(value)) minimumFormattedBytes += value.length;
334
+ if (
335
+ minimumFormattedBytes + CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES >=
336
+ CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
337
+ ) {
338
+ return undefined;
339
+ }
340
+ }
341
+ return denoInspectArgs(safeArgs, SAFE_DENO_INSPECT_OPTIONS);
342
+ }
343
+
209
344
  function assertNotebookBindingNameAvailable(name: string): void {
210
345
  if (isReservedNotebookBindingName(name)) {
211
346
  throw new typeErrorConstructor(`CodeMode Notebook Binding '${name}' is reserved`);
@@ -398,6 +533,35 @@ function utf8ByteLength(value: string): number {
398
533
  return encodeUtf8(value).byteLength;
399
534
  }
400
535
 
536
+ function utf8JsonStringByteLength(value: string): number {
537
+ let bytes = 2;
538
+ for (let index = 0; index < value.length; index += 1) {
539
+ const codeUnit = value.charCodeAt(index);
540
+ if (codeUnit === 0x22 || codeUnit === 0x5c) bytes += 2;
541
+ else if (codeUnit <= 0x1f) {
542
+ bytes +=
543
+ codeUnit === 0x08 ||
544
+ codeUnit === 0x09 ||
545
+ codeUnit === 0x0a ||
546
+ codeUnit === 0x0c ||
547
+ codeUnit === 0x0d
548
+ ? 2
549
+ : 6;
550
+ } else if (codeUnit <= 0x7f) bytes += 1;
551
+ else if (codeUnit <= 0x7ff) bytes += 2;
552
+ else if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
553
+ const lowSurrogate = value.charCodeAt(index + 1);
554
+ if (lowSurrogate >= 0xdc00 && lowSurrogate <= 0xdfff) {
555
+ bytes += 4;
556
+ index += 1;
557
+ } else bytes += 6;
558
+ } else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) bytes += 6;
559
+ else bytes += 3;
560
+ if (bytes >= CODEMODE_WORKER_MESSAGE_LIMIT_BYTES) return bytes;
561
+ }
562
+ return bytes;
563
+ }
564
+
401
565
  // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: This is arbitrary guest ingress; descriptor-only traversal avoids invoking accessors, coercion, and guest-mutated methods. Coordinator hostile JSON tests cover the boundary.
402
566
  function serializeGuestJson(value: unknown, allowUndefined: boolean): string | undefined {
403
567
  const seen: object[] = [];
@@ -715,6 +879,50 @@ defineProperty(globalThis, "tools", {
715
879
  value: tools,
716
880
  writable: false,
717
881
  });
882
+ const guestConsole = createObject(null);
883
+ for (const method of CODEMODE_CONSOLE_METHODS) {
884
+ defineProperty(guestConsole, method, {
885
+ configurable: false,
886
+ enumerable: true,
887
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: Cell Console calls accept arbitrary guest values; formatGuestConsoleArguments safely inspects them before capture.
888
+ value: (...args: unknown[]): undefined => {
889
+ const cell = activeCell;
890
+ if (cell !== undefined && !cell.consoleOverflow) {
891
+ const text = formatGuestConsoleArguments(args);
892
+ if (text === undefined) {
893
+ cell.consoleEntries.length = 0;
894
+ cell.consoleOverflow = true;
895
+ return undefined;
896
+ }
897
+ const entryBytes = 19 + utf8JsonStringByteLength(method) + utf8JsonStringByteLength(text);
898
+ const nextConsoleBytes =
899
+ cell.consoleBytes + (cell.consoleEntries.length === 0 ? 0 : 1) + entryBytes;
900
+ if (
901
+ nextConsoleBytes + CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES >=
902
+ CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
903
+ ) {
904
+ cell.consoleEntries.length = 0;
905
+ cell.consoleOverflow = true;
906
+ return undefined;
907
+ }
908
+ cell.consoleEntries[cell.consoleEntries.length] = {
909
+ method,
910
+ text,
911
+ };
912
+ cell.consoleBytes = nextConsoleBytes;
913
+ }
914
+ return undefined;
915
+ },
916
+ writable: false,
917
+ });
918
+ }
919
+ objectFreeze(guestConsole);
920
+ defineProperty(globalThis, "console", {
921
+ configurable: false,
922
+ enumerable: false,
923
+ value: guestConsole,
924
+ writable: false,
925
+ });
718
926
 
719
927
  function disableGuestGlobal(name: string, value?: Readonly<typeof safeDenoIdentity>): void {
720
928
  const descriptor = getOwnPropertyDescriptor(globalThis, name);
@@ -770,7 +978,6 @@ const safeDenoIdentity = objectFreeze({
770
978
  disableGuestGlobal("Deno", safeDenoIdentity);
771
979
  for (const unsafeGlobal of [
772
980
  "process",
773
- "console",
774
981
  "alert",
775
982
  "confirm",
776
983
  "prompt",
@@ -889,42 +1096,97 @@ function scheduleCellFinish(cell: ActiveWorkerCell): void {
889
1096
  return;
890
1097
  }
891
1098
  activeCell = undefined;
1099
+ const consoleEntries = cell.consoleEntries.length === 0 ? undefined : [...cell.consoleEntries];
892
1100
  let response: CodeModeWorkerResponse;
893
- if (cell.mainFailed) {
894
- const error = describeGuestError(cell.mainError);
895
- const serializationFailure =
896
- isGuestReference(cell.mainError) &&
897
- (hasSerializationErrorInstance(cell.mainError) ||
898
- internalToolErrorCode(cell.mainError) === "serialization");
1101
+ if (cell.consoleOverflow) {
899
1102
  response = {
900
1103
  version: 1,
901
1104
  type: "cell-error",
902
1105
  sessionId: cell.sessionId,
903
1106
  cellId: cell.cellId,
904
- error: {
905
- code: serializationFailure ? "serialization" : "script",
906
- message: renderGuestError(error),
907
- },
1107
+ error: { code: "serialization", message: "CodeMode worker response exceeds 8 MiB" },
908
1108
  };
909
- } else {
910
- try {
911
- const resultJson = serializeGuestJson(cell.mainResult, true);
1109
+ } else if (cell.mainFailed) {
1110
+ const error = describeGuestError(cell.mainError);
1111
+ const serializationFailure =
1112
+ isGuestReference(cell.mainError) &&
1113
+ (hasSerializationErrorInstance(cell.mainError) ||
1114
+ internalToolErrorCode(cell.mainError) === "serialization");
1115
+ const message = renderGuestError(error);
1116
+ if (
1117
+ consoleEntries !== undefined &&
1118
+ cell.consoleBytes +
1119
+ utf8JsonStringByteLength(message) +
1120
+ CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES >=
1121
+ CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
1122
+ ) {
1123
+ response = {
1124
+ version: 1,
1125
+ type: "cell-error",
1126
+ sessionId: cell.sessionId,
1127
+ cellId: cell.cellId,
1128
+ error: { code: "serialization", message: "CodeMode worker response exceeds 8 MiB" },
1129
+ };
1130
+ } else {
912
1131
  const responseBase = {
913
1132
  version: 1,
914
- type: "cell-result",
1133
+ type: "cell-error",
915
1134
  sessionId: cell.sessionId,
916
1135
  cellId: cell.cellId,
1136
+ error: {
1137
+ code: serializationFailure ? "serialization" : "script",
1138
+ message,
1139
+ },
917
1140
  } as const;
918
- response = resultJson === undefined ? responseBase : { ...responseBase, resultJson };
1141
+ response =
1142
+ consoleEntries === undefined
1143
+ ? responseBase
1144
+ : { ...responseBase, console: consoleEntries };
1145
+ }
1146
+ } else {
1147
+ try {
1148
+ const resultJson = serializeGuestJson(cell.mainResult, true);
1149
+ if (
1150
+ consoleEntries !== undefined &&
1151
+ cell.consoleBytes +
1152
+ (resultJson === undefined ? 0 : utf8JsonStringByteLength(resultJson)) +
1153
+ CODEMODE_CONSOLE_RESPONSE_RESERVE_BYTES >=
1154
+ CODEMODE_WORKER_MESSAGE_LIMIT_BYTES
1155
+ ) {
1156
+ response = {
1157
+ version: 1,
1158
+ type: "cell-error",
1159
+ sessionId: cell.sessionId,
1160
+ cellId: cell.cellId,
1161
+ error: { code: "serialization", message: "CodeMode worker response exceeds 8 MiB" },
1162
+ };
1163
+ } else {
1164
+ const responseBase = {
1165
+ version: 1,
1166
+ type: "cell-result",
1167
+ sessionId: cell.sessionId,
1168
+ cellId: cell.cellId,
1169
+ } as const;
1170
+ const resultResponse =
1171
+ resultJson === undefined ? responseBase : { ...responseBase, resultJson };
1172
+ response =
1173
+ consoleEntries === undefined
1174
+ ? resultResponse
1175
+ : { ...resultResponse, console: consoleEntries };
1176
+ }
919
1177
  } catch (cause) {
920
1178
  const error = describeGuestError(cause);
921
- response = {
1179
+ const responseBase = {
922
1180
  version: 1,
923
1181
  type: "cell-error",
924
1182
  sessionId: cell.sessionId,
925
1183
  cellId: cell.cellId,
926
1184
  error: { code: "serialization", message: renderGuestError(error) },
927
- };
1185
+ } as const;
1186
+ response =
1187
+ consoleEntries === undefined
1188
+ ? responseBase
1189
+ : { ...responseBase, console: consoleEntries };
928
1190
  }
929
1191
  }
930
1192
  void enqueueWorkerResponse(response);
@@ -939,6 +1201,9 @@ function startWorkerCell(
939
1201
  sessionId: request.sessionId,
940
1202
  cellId: request.cellId,
941
1203
  pendingCalls: [],
1204
+ consoleEntries: [],
1205
+ consoleBytes: 2,
1206
+ consoleOverflow: false,
942
1207
  batchSequence: 0,
943
1208
  callSequence: 0,
944
1209
  batchScheduled: false,
@@ -39,7 +39,7 @@ import {
39
39
  } from "./pi-tool-bridge.js";
40
40
 
41
41
  const CODEMODE_EXECUTE_DESCRIPTION =
42
- "Execute a TypeScript Cell in a persistent isolated Deno CodeMode Session. Reuse a Session ID to retain Notebook Bindings; a new Session reclaims the least-recently-used idle Session at capacity. Use the read-only tools object for registered Pi tools.";
42
+ "Execute a TypeScript Cell in a persistent isolated Deno CodeMode Session. Reuse a Session ID to retain Notebook Bindings; a new Session reclaims the least-recently-used idle Session at capacity. Use the read-only tools object for registered Pi tools. Cells may call console.log, console.info, console.warn, console.error, and console.debug; captured output arrives only with terminal results.";
43
43
 
44
44
  type PiCodeModeGeneration = {
45
45
  readonly captured: CapturedPiAgentSession;
@@ -126,11 +126,11 @@ class PiCodeModeLifecycleController {
126
126
  this.pi.on("session_start", async (_event, context) => this.startSession(context));
127
127
  this.pi.on("before_agent_start", () => this.synchronizeCurrentGeneration());
128
128
  this.pi.on("tool_execution_end", () => this.synchronizeCurrentGeneration());
129
- this.pi.on("session_shutdown", async (event) => this.shutdownSession(event.reason));
129
+ this.pi.on("session_shutdown", async () => this.shutdownSession());
130
130
  }
131
131
 
132
132
  private async startSession(context: ExtensionContext): Promise<void> {
133
- await this.shutdownSession("replacement");
133
+ await this.shutdownSession();
134
134
  const capturedResult = capturePiAgentSession(this.pi);
135
135
  if (!capturedResult.ok) {
136
136
  this.notifyWarning(context, capturedResult.warning);
@@ -247,7 +247,7 @@ class PiCodeModeLifecycleController {
247
247
  } catch {
248
248
  // Observer cleanup is presentation-only; execution resources still require release.
249
249
  }
250
- await coordinator.shutdown("startup failure");
250
+ await coordinator.shutdown();
251
251
  await sessionFiles.close();
252
252
  if (this.generation === generation) this.generation = undefined;
253
253
  this.notifyWarning(
@@ -389,12 +389,8 @@ class PiCodeModeLifecycleController {
389
389
  now: CODEMODE_SYSTEM_RUNTIME.now,
390
390
  signal: AbortSignal.any([batch.signal, terminationController.signal]),
391
391
  onTerminate: () => terminationController.abort(),
392
- };
393
- if (outerAssistantMessage !== undefined) {
394
- Object.assign(bridgeOptions, { outerAssistantMessage });
395
- }
396
- if (batch.onUpdate !== undefined) {
397
- Object.assign(bridgeOptions, {
392
+ ...(outerAssistantMessage !== undefined && { outerAssistantMessage }),
393
+ ...(batch.onUpdate !== undefined && {
398
394
  onUpdate: (_callId: string, update: AgentToolResult<unknown>) => {
399
395
  const outerUpdate: AgentToolResult<CodeModeResultDetails> = {
400
396
  content: update.content,
@@ -402,8 +398,8 @@ class PiCodeModeLifecycleController {
402
398
  };
403
399
  batch.onUpdate?.(outerUpdate);
404
400
  },
405
- });
406
- }
401
+ }),
402
+ };
407
403
  const bridged = await executePiToolBridgeBatch(bridgeCaptured, bridgeOptions);
408
404
  const bridgedResults = new Map<string, CodeModeNestedToolResult>(
409
405
  bridged.calls.map((outcome) => [
@@ -431,16 +427,16 @@ class PiCodeModeLifecycleController {
431
427
  "Pi CodeMode nested tool returned no result",
432
428
  ),
433
429
  );
434
- const batchResult = { results, presentation: bridged.presentation };
435
- if (bridged.usage !== undefined) Object.assign(batchResult, { usage: bridged.usage });
436
- if (bridged.addedToolNames.length > 0) {
437
- Object.assign(batchResult, { addedToolNames: bridged.addedToolNames });
438
- }
439
- if (bridged.terminate) Object.assign(batchResult, { terminate: true });
440
- return batchResult;
430
+ return {
431
+ results,
432
+ presentation: bridged.presentation,
433
+ ...(bridged.usage !== undefined && { usage: bridged.usage }),
434
+ ...(bridged.addedToolNames.length > 0 && { addedToolNames: bridged.addedToolNames }),
435
+ ...(bridged.terminate && { terminate: true }),
436
+ };
441
437
  }
442
438
 
443
- private async shutdownSession(reason: string): Promise<void> {
439
+ private async shutdownSession(): Promise<void> {
444
440
  const generation = this.generation;
445
441
  if (generation === undefined || !generation.active) return;
446
442
  generation.active = false;
@@ -450,7 +446,7 @@ class PiCodeModeLifecycleController {
450
446
  // Observer cleanup is presentation-only; execution resources still require release.
451
447
  }
452
448
  try {
453
- await generation.coordinator.shutdown(reason);
449
+ await generation.coordinator.shutdown();
454
450
  } finally {
455
451
  try {
456
452
  await generation.sessionFiles.close();
@@ -12,6 +12,7 @@ import {
12
12
  parseCodeModeJsonValue,
13
13
  type CodeModeJsonValue,
14
14
  } from "./codemode-tool-contract.js";
15
+ import { addCodeModeUsage, boundedCodeModeElapsedMs } from "./codemode-session-coordinator.js";
15
16
  import type { CapturedPiAgentSession } from "./pi-agent-session-capture.js";
16
17
 
17
18
  const PI_TOOL_BRIDGE_RESULT_LIMIT_BYTES = 8 * 1024 * 1024;
@@ -505,36 +506,6 @@ function translatePiToolResult(
505
506
  };
506
507
  }
507
508
 
508
- function addUsage(left: Usage | undefined, right: Usage | undefined): Usage | undefined {
509
- if (right === undefined) return left;
510
- if (left === undefined) return right;
511
- const reasoning =
512
- left.reasoning === undefined && right.reasoning === undefined
513
- ? undefined
514
- : (left.reasoning ?? 0) + (right.reasoning ?? 0);
515
- const cacheWrite1h =
516
- left.cacheWrite1h === undefined && right.cacheWrite1h === undefined
517
- ? undefined
518
- : (left.cacheWrite1h ?? 0) + (right.cacheWrite1h ?? 0);
519
- const combined: Usage = {
520
- input: left.input + right.input,
521
- output: left.output + right.output,
522
- cacheRead: left.cacheRead + right.cacheRead,
523
- cacheWrite: left.cacheWrite + right.cacheWrite,
524
- totalTokens: left.totalTokens + right.totalTokens,
525
- cost: {
526
- input: left.cost.input + right.cost.input,
527
- output: left.cost.output + right.cost.output,
528
- cacheRead: left.cost.cacheRead + right.cost.cacheRead,
529
- cacheWrite: left.cost.cacheWrite + right.cost.cacheWrite,
530
- total: left.cost.total + right.cost.total,
531
- },
532
- };
533
- if (cacheWrite1h !== undefined) combined.cacheWrite1h = cacheWrite1h;
534
- if (reasoning !== undefined) combined.reasoning = reasoning;
535
- return combined;
536
- }
537
-
538
509
  function terminatedPiToolCall(callId: string): FinalizedPiToolCall {
539
510
  return createBridgeFailure(
540
511
  callId,
@@ -553,12 +524,6 @@ function presentationOutcome(
553
524
  : "failed";
554
525
  }
555
526
 
556
- function elapsedMilliseconds(startedAt: number, finishedAt: number): number {
557
- const elapsed = Math.round(finishedAt - startedAt);
558
- if (!Number.isFinite(elapsed)) return 0;
559
- return Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, elapsed));
560
- }
561
-
562
527
  function timedFinalizedPiToolCall(
563
528
  call: PiToolBridgeCall,
564
529
  finalized: FinalizedPiToolCall,
@@ -569,7 +534,7 @@ function timedFinalizedPiToolCall(
569
534
  finalized,
570
535
  presentation: {
571
536
  callId: call.callId,
572
- elapsedMs: elapsedMilliseconds(startedAt, now()),
537
+ elapsedMs: boundedCodeModeElapsedMs(startedAt, now()),
573
538
  name: call.name,
574
539
  outcome: presentationOutcome(finalized.outcome),
575
540
  },
@@ -590,7 +555,9 @@ function collectPiToolBridgeBatch(
590
555
  const addedToolNames: string[] = [];
591
556
  const seenToolNames = new Set<string>();
592
557
  for (const { finalized } of timedCalls) {
593
- usage = addUsage(usage, finalized.usage);
558
+ if (finalized.usage !== undefined) {
559
+ usage = addCodeModeUsage(usage, finalized.usage);
560
+ }
594
561
  for (const toolName of finalized.addedToolNames) {
595
562
  if (seenToolNames.has(toolName)) continue;
596
563
  seenToolNames.add(toolName);