@pasko70/pibo 3.4.3 → 3.5.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.
Files changed (84) hide show
  1. package/dist/agent-runtime/routed-session.js +39 -12
  2. package/dist/agent-runtimes/codex-native/adapter.js +14 -1
  3. package/dist/agent-runtimes/codex-native/models.js +4 -4
  4. package/dist/agent-runtimes/codex-native/process.js +32 -13
  5. package/dist/agent-runtimes/codex-native/provider-usage.js +115 -0
  6. package/dist/agent-runtimes/pi/adapter.js +1 -0
  7. package/dist/agent-runtimes/pi/routed-session.js +4 -2
  8. package/dist/apps/chat/bounded-event-stream.js +98 -0
  9. package/dist/apps/chat/chat-settings-routes.js +3 -3
  10. package/dist/apps/chat/data/chat-data-mappers.js +32 -13
  11. package/dist/apps/chat/data/event-command-service.js +30 -21
  12. package/dist/apps/chat/data/history-query-service.js +34 -25
  13. package/dist/apps/chat/data/read-state-service.js +13 -0
  14. package/dist/apps/chat/data/session-query-service.js +19 -11
  15. package/dist/apps/chat/data/timeline-query-service.js +19 -7
  16. package/dist/apps/chat/message-command-dispatcher.js +134 -0
  17. package/dist/apps/chat/output-compactor.js +9 -0
  18. package/dist/apps/chat/output-event-policy.js +9 -1
  19. package/dist/apps/chat/stream.js +21 -3
  20. package/dist/apps/chat/telemetry-retention-service.js +113 -8
  21. package/dist/apps/chat/trace-response-cache.js +46 -0
  22. package/dist/apps/chat/trace-v2.js +12 -6
  23. package/dist/apps/chat/trace.js +1 -1
  24. package/dist/apps/chat/web-app.js +608 -281
  25. package/dist/apps/chat-ui/assets/{dist-DeMKnZR8.js → dist-BG0n7zLd.js} +1 -1
  26. package/dist/apps/chat-ui/assets/{dist-C8GzUMPk.js → dist-D6TjFhAm.js} +1 -1
  27. package/dist/apps/chat-ui/assets/{dist-BkUu8WPA.js → dist-D79vyxSX.js} +1 -1
  28. package/dist/apps/chat-ui/assets/{dist-B4YhOxh0.js → dist-DFZ8cwh0.js} +1 -1
  29. package/dist/apps/chat-ui/assets/{dist-Cyb4rVa6.js → dist-cOjokPrK.js} +1 -1
  30. package/dist/apps/chat-ui/assets/index-RMHUTJ62.js +229 -0
  31. package/dist/apps/chat-ui/assets/{index-BOceJ0jM.css → index-hEkrlRk-.css} +1 -1
  32. package/dist/apps/chat-ui/index.html +2 -2
  33. package/dist/apps/chat-vscode-web/assets/index-xacbCyTx.js +44 -0
  34. package/dist/apps/chat-vscode-web/index.html +1 -1
  35. package/dist/compute/pool/seeds.js +25 -3
  36. package/dist/core/events.js +4 -0
  37. package/dist/core/output-render-sequence.js +2 -0
  38. package/dist/core/provider-capacity.js +33 -0
  39. package/dist/core/provider-telemetry.js +21 -6
  40. package/dist/core/runtime-capacity.js +174 -0
  41. package/dist/core/runtime-telemetry.js +40 -12
  42. package/dist/core/session-router.js +79 -2
  43. package/dist/data/async-chat-reads.js +38 -0
  44. package/dist/data/async-chat-storage.js +91 -0
  45. package/dist/data/async-telemetry-maintenance.js +9 -0
  46. package/dist/data/bounded-worker-client.js +256 -0
  47. package/dist/data/chat-read-projections.js +159 -0
  48. package/dist/data/chat-read-worker.js +73 -0
  49. package/dist/data/chat-storage-worker.js +146 -0
  50. package/dist/data/ingest-service.js +79 -14
  51. package/dist/data/message-command-store.js +148 -0
  52. package/dist/data/payload-store.js +92 -11
  53. package/dist/data/pibo-store.js +10 -8
  54. package/dist/data/schema.js +16 -2
  55. package/dist/data/session-store.js +3 -1
  56. package/dist/data/storage-backup.js +278 -0
  57. package/dist/data/telemetry-capture.js +188 -0
  58. package/dist/data/telemetry-command.js +3 -0
  59. package/dist/data/telemetry-maintenance-worker.js +40 -0
  60. package/dist/data/telemetry-maintenance.js +110 -0
  61. package/dist/data/telemetry-retention.js +16 -7
  62. package/dist/data/telemetry-worker.js +111 -0
  63. package/dist/data/telemetry-writer.js +150 -83
  64. package/dist/data/telemetry.js +5 -0
  65. package/dist/debug/index.js +52 -0
  66. package/dist/debug/storage-backup.js +33 -0
  67. package/dist/debug/telemetry-capture.js +66 -0
  68. package/dist/gateway/cli.js +104 -16
  69. package/dist/gateway/server.js +2 -0
  70. package/dist/providers/openai-gpt56.js +11 -6
  71. package/dist/session-ui/terminalRows.js +55 -13
  72. package/dist/sessions/pibo-data-store.js +30 -10
  73. package/dist/shared/debug-features.js +4 -0
  74. package/dist/shared/model-inference-metrics.js +23 -0
  75. package/dist/shared/trace-event-projection.js +59 -2
  76. package/dist/shared/trace-history.js +9 -0
  77. package/dist/shared/trace-live-reducer.js +1 -0
  78. package/dist/shared/trace-patch-nodes.js +19 -0
  79. package/dist/web/channel.js +8 -2
  80. package/dist/web/http.js +36 -3
  81. package/npm-shrinkwrap.json +2 -2
  82. package/package.json +1 -1
  83. package/dist/apps/chat-ui/assets/index-Dk4mbXAB.js +0 -228
  84. package/dist/apps/chat-vscode-web/assets/index-0oTGFHni.js +0 -43
@@ -0,0 +1,33 @@
1
+ import { parseArgs } from "node:util";
2
+ import { createStorageBackup, readStorageBackupManifest, verifyStorageBackup, restoreStorageBackup } from "../data/storage-backup.js";
3
+ export async function runStorageBackupCli(args) {
4
+ const action = args[0];
5
+ if (!action || args.includes("--help") || args.includes("-h")) {
6
+ console.log(`pibo debug backup - explicit SQLite snapshot and payload backup
7
+ Commands:
8
+ create --source <sqlite> --payload-root <dir> --destination <new-dir>
9
+ [--resume] [--max-bytes <n>] [--max-payloads <n>] [--max-ms <n>] [--max-wal-growth <n>]
10
+ inspect --archive <dir> Read the manifest only
11
+ verify --archive <dir> Verify database and all payload hashes
12
+ restore --archive <dir> --destination <new-dir>
13
+ Defaults: 1 GiB data, 100000 payloads, 60000 ms and 64 MiB extra source WAL per create attempt. Resume preserves the original snapshot.
14
+ Each archive contains one database snapshot. Product and reliability snapshots are separate cuts.
15
+ No source deletion, vacuum, or overwrite of an existing restore destination.`);
16
+ return;
17
+ }
18
+ const { values } = parseArgs({ args: args.slice(1), options: { source: { type: "string" }, "payload-root": { type: "string" }, destination: { type: "string" }, archive: { type: "string" }, resume: { type: "boolean" }, "max-bytes": { type: "string" }, "max-payloads": { type: "string" }, "max-ms": { type: "string" }, "max-wal-growth": { type: "string" }, json: { type: "boolean" } } });
19
+ const required = (name) => { const value = values[name]; if (typeof value !== "string" || !value)
20
+ throw Error(`Missing --${name}`); return value; };
21
+ let result;
22
+ if (action === "create")
23
+ result = await createStorageBackup({ source: required("source"), payloadRoot: required("payload-root"), destination: required("destination"), resume: values.resume, maxBytes: values["max-bytes"] ? Number(values["max-bytes"]) : undefined, maxPayloads: values["max-payloads"] ? Number(values["max-payloads"]) : undefined, maxMilliseconds: values["max-ms"] ? Number(values["max-ms"]) : undefined, maxWalGrowthBytes: values["max-wal-growth"] ? Number(values["max-wal-growth"]) : undefined });
24
+ else if (action === "inspect")
25
+ result = await readStorageBackupManifest(required("archive"));
26
+ else if (action === "verify")
27
+ result = await verifyStorageBackup(required("archive"), AbortSignal.timeout(60000));
28
+ else if (action === "restore")
29
+ result = await restoreStorageBackup(required("archive"), required("destination"), AbortSignal.timeout(60000));
30
+ else
31
+ throw Error("Unknown backup action; use backup --help");
32
+ console.log(JSON.stringify(result, null, 2));
33
+ }
@@ -0,0 +1,66 @@
1
+ import { parseArgs } from "node:util";
2
+ import { join, dirname } from "node:path";
3
+ import { opendir } from "node:fs/promises";
4
+ import { resolveDebugStore } from "./stores.js";
5
+ import { startTelemetryCapture, inspectTelemetryCapture, finalizeTelemetryCapture, readTelemetryCapturePage } from "../data/telemetry-capture.js";
6
+ export async function runTelemetryCaptureCli(args) {
7
+ const action = args[0];
8
+ if (!action || args.includes("--help") || args.includes("-h")) {
9
+ console.log(`pibo debug telemetry capture - scoped provider metadata in isolated stores
10
+ Commands:
11
+ start --session <ps_...> --owner <name> --duration-ms <n> --max-bytes <n> --max-rows <n>
12
+ stop <capture-id> Fence appends and finalize an independent closed archive
13
+ list List up to 100 manifests without opening databases
14
+ inspect <capture-id> Read one manifest without opening its database
15
+ page <capture-id> [--after <sequence>] [--limit <1..100>]
16
+ Limits: one active capture, at most one hour, 64 MiB logical data and 100000 rows per run.
17
+ Detail is allow-listed provider metadata only. Arbitrary provider bodies, credentials and prompts are excluded.
18
+ Stopping/finalizing is explicit; archived data is never opened by normal gateway reads.`);
19
+ return;
20
+ }
21
+ const { values, positionals } = parseArgs({ args: args.slice(1), allowPositionals: true, options: { session: { type: "string" }, owner: { type: "string" }, "duration-ms": { type: "string" }, "max-bytes": { type: "string" }, "max-rows": { type: "string" }, after: { type: "string" }, limit: { type: "string" }, json: { type: "boolean" } } });
22
+ const root = join(dirname(resolveDebugStore("pibo-data").path), "telemetry-captures");
23
+ let result;
24
+ if (action === "start")
25
+ result = startTelemetryCapture(root, { sessionId: values.session ?? "", owner: values.owner ?? "", durationMs: Number(values["duration-ms"]), maxBytes: Number(values["max-bytes"]), maxRows: Number(values["max-rows"]) });
26
+ else if (action === "list") {
27
+ const entries = [];
28
+ let scanned = 0, truncated = false;
29
+ try {
30
+ const directory = await opendir(root);
31
+ for await (const entry of directory) {
32
+ if (++scanned > 1000 || entries.length >= 100) {
33
+ truncated = true;
34
+ break;
35
+ }
36
+ if (!entry.isDirectory() || !entry.name.startsWith("capture_"))
37
+ continue;
38
+ try {
39
+ entries.push(inspectTelemetryCapture(root, entry.name));
40
+ }
41
+ catch {
42
+ entries.push({ id: entry.name, status: "manifest_unavailable" });
43
+ }
44
+ }
45
+ }
46
+ catch (error) {
47
+ if (error.code !== "ENOENT")
48
+ throw error;
49
+ }
50
+ result = { entries, truncated };
51
+ }
52
+ else {
53
+ const id = positionals[0];
54
+ if (!id)
55
+ throw Error("Capture id is required");
56
+ if (action === "stop")
57
+ result = await finalizeTelemetryCapture(root, id);
58
+ else if (action === "inspect")
59
+ result = inspectTelemetryCapture(root, id);
60
+ else if (action === "page")
61
+ result = await readTelemetryCapturePage(root, id, Number(values.after ?? 0), Number(values.limit ?? 50));
62
+ else
63
+ throw Error("Unknown capture action; use capture --help");
64
+ }
65
+ console.log(JSON.stringify(result, null, 2));
66
+ }
@@ -1,5 +1,6 @@
1
1
  import { spawn, execFile } from "node:child_process";
2
- import { existsSync, mkdirSync, openSync, readFileSync, readdirSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { appendFileSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync } from "node:fs";
3
4
  import { homedir } from "node:os";
4
5
  import { join } from "node:path";
5
6
  import { promisify } from "node:util";
@@ -205,11 +206,14 @@ function runtimeTelemetryHint(value) {
205
206
  }
206
207
  function runtimeStatus(value) {
207
208
  const obj = objectValue(value);
208
- if (!obj)
209
+ if (!obj || !stringValue(obj.piboSessionId) || typeof obj.processing !== "boolean" || typeof obj.streaming !== "boolean"
210
+ || !Number.isInteger(obj.queuedMessages) || Number(obj.queuedMessages) < 0)
209
211
  return undefined;
210
212
  return {
211
213
  piboSessionId: stringValue(obj.piboSessionId),
212
214
  queuedMessages: numberValue(obj.queuedMessages),
215
+ activeEventId: stringValue(obj.activeEventId),
216
+ queuedEventIds: Array.isArray(obj.queuedEventIds) && obj.queuedEventIds.every((id) => typeof id === "string") ? obj.queuedEventIds : undefined,
213
217
  processing: booleanValue(obj.processing),
214
218
  streaming: booleanValue(obj.streaming),
215
219
  activeTelemetry: runtimeTelemetryHint(obj.activeTelemetry),
@@ -217,13 +221,14 @@ function runtimeStatus(value) {
217
221
  }
218
222
  function activeRun(value) {
219
223
  const obj = objectValue(value);
220
- if (!obj)
224
+ if (!obj || !stringValue(obj.runId) || !stringValue(obj.status)
225
+ || !(stringValue(obj.controllerPiboSessionId) || stringValue(obj.piboSessionId)))
221
226
  return undefined;
222
227
  return {
223
228
  runId: stringValue(obj.runId),
224
229
  status: stringValue(obj.status),
225
230
  toolName: stringValue(obj.toolName),
226
- piboSessionId: stringValue(obj.piboSessionId),
231
+ piboSessionId: stringValue(obj.controllerPiboSessionId) ?? stringValue(obj.piboSessionId),
227
232
  };
228
233
  }
229
234
  function parseGatewaySafetyPayload(payload, reachable) {
@@ -231,7 +236,9 @@ function parseGatewaySafetyPayload(payload, reachable) {
231
236
  const mode = obj && (obj.mode === "dev" || obj.mode === "prod" || obj.mode === "fallback") ? obj.mode : "unknown";
232
237
  const runtimeStatuses = Array.isArray(obj?.runtimeStatuses) ? obj.runtimeStatuses.map(runtimeStatus).filter((item) => Boolean(item)) : [];
233
238
  const activeRuns = Array.isArray(obj?.activeRuns) ? obj.activeRuns.map(activeRun).filter((item) => Boolean(item)) : [];
234
- return { reachable, mode, health: obj?.health, runtimeStatuses, activeRuns, ambiguous: booleanValue(obj?.ambiguous) };
239
+ const incomplete = !Array.isArray(obj?.runtimeStatuses) || !Array.isArray(obj?.activeRuns)
240
+ || runtimeStatuses.length !== obj.runtimeStatuses.length || activeRuns.length !== obj.activeRuns.length;
241
+ return { reachable, mode, generation: stringValue(obj?.generation), health: obj?.health, runtimeStatuses, activeRuns, ambiguous: incomplete || booleanValue(obj?.ambiguous) };
235
242
  }
236
243
  export function checkActiveWork(status, target = "web") {
237
244
  const reasons = [];
@@ -249,7 +256,7 @@ export function checkActiveWork(status, target = "web") {
249
256
  reasons.push(`${id} is processing`);
250
257
  if (session.streaming === true)
251
258
  reasons.push(`${id} is streaming`);
252
- if ((session.queuedMessages ?? 0) > 0)
259
+ if ((session.queuedMessages ?? 0) > 0 || (session.activeTelemetry?.queueDepth ?? 0) > 0)
253
260
  reasons.push(`${id} has queued messages`);
254
261
  if (session.activeTelemetry?.isStale === true) {
255
262
  const phase = session.activeTelemetry.activePhase ? ` in ${session.activeTelemetry.activePhase}` : "";
@@ -260,6 +267,58 @@ export function checkActiveWork(status, target = "web") {
260
267
  reasons.push(`${run.runId ?? "yielded run"} is ${run.status ?? "active"}`);
261
268
  return { unsafe: reasons.length > 0, reasons };
262
269
  }
270
+ function blockingSessions(status) {
271
+ return status.runtimeStatuses.filter((session) => session.processing || session.streaming
272
+ || (session.queuedMessages ?? 0) > 0 || (session.activeTelemetry?.queueDepth ?? 0) > 0 || session.activeTelemetry?.isStale);
273
+ }
274
+ export function restartConfirmationToken(status) {
275
+ if (!status.reachable || status.error || status.ambiguous || status.mode !== "prod" || !status.generation)
276
+ return undefined;
277
+ const sessions = blockingSessions(status);
278
+ if (status.runtimeStatuses.some((session) => !session.piboSessionId || typeof session.processing !== "boolean"
279
+ || typeof session.streaming !== "boolean" || !Number.isInteger(session.queuedMessages) || session.queuedMessages < 0))
280
+ return undefined;
281
+ if (sessions.some((session) => ((session.processing || session.streaming || session.activeTelemetry?.isStale)
282
+ && !(session.activeEventId || session.activeTelemetry?.activeTurnId))
283
+ || (session.queuedMessages > 0 && (session.queuedEventIds?.length !== session.queuedMessages || session.queuedEventIds?.some((id) => !id)))
284
+ || (session.activeTelemetry?.queueDepth ?? 0) > (session.queuedMessages ?? 0)))
285
+ return undefined;
286
+ if (status.activeRuns.some((run) => !run.runId || !run.piboSessionId || !run.status))
287
+ return undefined;
288
+ // Exclude clock/progress counters: approval describes work identities, not polling time.
289
+ const snapshot = {
290
+ generation: status.generation,
291
+ target: "web", port: targetPort("web"), service: gatewayServiceName("web"), home: managedGatewayHome("web"),
292
+ sessions: sessions.map((session) => ({
293
+ id: session.piboSessionId, activeEventId: session.activeEventId,
294
+ processing: session.processing, streaming: session.streaming,
295
+ queuedEventIds: [...(session.queuedEventIds ?? [])], queuedMessages: session.queuedMessages,
296
+ telemetryTurnId: session.activeTelemetry?.activeTurnId,
297
+ stale: session.activeTelemetry?.isStale === true,
298
+ })).sort((a, b) => a.id.localeCompare(b.id)),
299
+ runs: [...status.activeRuns].sort((a, b) => a.runId.localeCompare(b.runId)),
300
+ };
301
+ return `${RESTART_CONFIRMATION_TOKEN}:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
302
+ }
303
+ function printRestartApproval(status) {
304
+ const token = restartConfirmationToken(status);
305
+ if (token) {
306
+ console.log("Review the listed work and obtain user approval before running:");
307
+ console.log(` pibo gateway web restart --force --confirm ${token}`);
308
+ }
309
+ else
310
+ console.log("Snapshot approval unavailable: status must identify the gateway generation and all active work. Inspect: pibo gateway web doctor");
311
+ }
312
+ function auditRestart(status, decision, force, reason) {
313
+ const home = managedGatewayHome("web");
314
+ mkdirSync(home, { recursive: true });
315
+ appendFileSync(join(home, "gateway-restart-audit.jsonl"), `${JSON.stringify({
316
+ at: new Date().toISOString(), decision, force, reason, snapshot: restartConfirmationToken(status),
317
+ generation: status.generation,
318
+ sessionIds: [...new Set([...blockingSessions(status).map((session) => session.piboSessionId), ...status.activeRuns.map((run) => run.piboSessionId)].filter(Boolean))],
319
+ runIds: status.activeRuns.map((run) => run.runId), reasons: checkActiveWork(status).reasons,
320
+ })}\n`, { mode: 0o600 });
321
+ }
263
322
  async function readGatewaySafetyStatus(target) {
264
323
  const port = targetPort(target);
265
324
  const reachable = await isPortReachable("127.0.0.1", port, 1500);
@@ -281,6 +340,10 @@ function printSafetyStatus(target, status) {
281
340
  console.log(` runtime sessions: ${status.runtimeStatuses.length}`);
282
341
  for (const session of status.runtimeStatuses) {
283
342
  console.log(` ${session.piboSessionId ?? "unknown"}: processing=${session.processing === true} streaming=${session.streaming === true} queued=${session.queuedMessages ?? 0}`);
343
+ if (session.activeEventId)
344
+ console.log(` active event: ${session.activeEventId}`);
345
+ if (session.queuedEventIds?.length)
346
+ console.log(` queued events: ${session.queuedEventIds.join(", ")}`);
284
347
  if (session.activeTelemetry) {
285
348
  const parts = [
286
349
  session.activeTelemetry.activePhase ? `phase=${session.activeTelemetry.activePhase}` : undefined,
@@ -295,7 +358,7 @@ function printSafetyStatus(target, status) {
295
358
  }
296
359
  console.log(` active yielded runs: ${status.activeRuns.length}`);
297
360
  for (const run of status.activeRuns)
298
- console.log(` ${run.runId ?? "unknown"}: ${run.status ?? "active"}${run.toolName ? ` (${run.toolName})` : ""}`);
361
+ console.log(` ${run.runId ?? "unknown"}: ${run.status ?? "active"}${run.toolName ? ` (${run.toolName})` : ""} session=${run.piboSessionId ?? "unknown"}`);
299
362
  }
300
363
  function managerRequiresShell(command) {
301
364
  return process.platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
@@ -332,6 +395,7 @@ async function runManagedGatewayCommand(target, command, args, argv = process.ar
332
395
  }
333
396
  else
334
397
  console.log(" restart safety: idle");
398
+ printRestartApproval(status);
335
399
  }
336
400
  if (command === "doctor")
337
401
  process.exitCode = status.reachable && !status.error && status.mode === expectedMode(target) ? 0 : 1;
@@ -380,19 +444,43 @@ async function runManagedGatewayCommand(target, command, args, argv = process.ar
380
444
  const force = args.includes("--force");
381
445
  const confirmIndex = args.indexOf("--confirm");
382
446
  const confirmation = confirmIndex >= 0 ? args[confirmIndex + 1] : undefined;
383
- if (force && confirmation !== RESTART_CONFIRMATION_TOKEN) {
447
+ if (target === "dev" && force && confirmation !== RESTART_CONFIRMATION_TOKEN) {
384
448
  console.error(`Force restart requires: --confirm ${RESTART_CONFIRMATION_TOKEN}`);
385
449
  process.exitCode = 1;
386
450
  return true;
387
451
  }
388
- if (target === "web" && !force) {
389
- const active = checkActiveWork(await readGatewaySafetyStatus(target), target);
390
- if (active.unsafe) {
391
- console.error("Restart blocked: active agent work is running.");
392
- console.error("Do not restart the gateway now.");
393
- console.error("Ask the user before interrupting active sessions.");
452
+ if (target === "web") {
453
+ // Both normal and delayed/forced commands inspect again at execution time.
454
+ let current = await readGatewaySafetyStatus(target);
455
+ for (let inspection = 0; inspection < 2; inspection += 1) {
456
+ const active = checkActiveWork(current, target);
457
+ const token = restartConfirmationToken(current);
458
+ printSafetyStatus(target, current);
394
459
  for (const reason of active.reasons)
395
460
  console.error(`- ${reason}`);
461
+ const blocked = force ? !token || confirmation !== token : active.unsafe;
462
+ if (blocked) {
463
+ const reason = force ? "Snapshot approval is missing, unavailable, or changed." : "Active work or unavailable gateway status.";
464
+ console.error(`Restart blocked: ${reason}`);
465
+ console.error("Ask the user before interrupting active sessions. Obtain fresh approval with: pibo gateway web status");
466
+ printRestartApproval(current);
467
+ try {
468
+ auditRestart(current, "blocked", force, reason);
469
+ }
470
+ catch (error) {
471
+ console.error(`Restart audit failed: ${error instanceof Error ? error.message : String(error)}`);
472
+ }
473
+ process.exitCode = 1;
474
+ return true;
475
+ }
476
+ if (inspection === 0)
477
+ current = await readGatewaySafetyStatus(target);
478
+ }
479
+ try {
480
+ auditRestart(current, "approved", force, force ? "Unchanged snapshot explicitly approved" : "Gateway idle");
481
+ }
482
+ catch (error) {
483
+ console.error(`Restart blocked: cannot record restart audit: ${error instanceof Error ? error.message : String(error)}`);
396
484
  process.exitCode = 1;
397
485
  return true;
398
486
  }
@@ -642,8 +730,8 @@ Commands:
642
730
  dev doctor Check dev gateway health
643
731
 
644
732
  Options:
645
- --force --confirm ${RESTART_CONFIRMATION_TOKEN}
646
- Force a production restart after explicit confirmation
733
+ --force --confirm <snapshot-token>
734
+ Restart only the work explicitly approved from web status
647
735
 
648
736
  Next:
649
737
  pibo gateway web status
@@ -360,6 +360,7 @@ export class PiboGatewayServer {
360
360
  },
361
361
  findSessions: (input) => this.requireSessionStore().find(input),
362
362
  listSessions: () => this.requireSessionStore().list?.() ?? [],
363
+ getSessionStructureRevision: () => this.requireSessionStore().getStructureRevision?.(),
363
364
  getSessionRuntimeBinding: (piboSessionId) => this.requireRouter().getSessionRuntimeBinding(piboSessionId),
364
365
  getSessionRuntimeProfile: (piboSessionId) => this.requireRouter().getSessionRuntimeProfile(piboSessionId),
365
366
  inspectSessionRuntimeHistory: async (piboSessionId) => {
@@ -424,6 +425,7 @@ export class PiboGatewayServer {
424
425
  getSessionStatusSnapshot: (piboSessionId, options) => this.requireRouter().getSessionStatusSnapshot(piboSessionId, options),
425
426
  getSessionForkCandidates: (piboSessionId) => this.requireRouter().getSessionForkCandidates(piboSessionId),
426
427
  listSessionRuntimeStatuses: () => this.requireRouter().listSessionRuntimeStatuses(),
428
+ getRuntimeCapacityStatus: () => this.requireRouter().getRuntimeCapacityStatus(),
427
429
  listRuns: (options) => this.requireRouter().listRuns(options),
428
430
  snapshotSignalSession: (piboSessionId) => this.requireRouter().snapshotSignalSession(piboSessionId),
429
431
  snapshotSignalTree: (rootPiboSessionId) => this.requireRouter().snapshotSignalTree(rootPiboSessionId),
@@ -38,6 +38,9 @@ export const OPENAI_CODEX_GPT_6_ASTRA_MODEL = {
38
38
  name: "GPT-6-Astra",
39
39
  cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
40
40
  };
41
+ // Codex serves the Reserve quota alias from the Luna-class model, so Reserve borrows Luna's metadata.
42
+ export const OPENAI_CODEX_RESERVE_MODEL = { id: "gpt-reserve", name: "Luna Reserve" };
43
+ const OPENAI_CODEX_RESERVE_BASE_MODEL_ID = "gpt-5.6-luna";
41
44
  const OPENAI_GPT_56_MODEL_IDS = new Set(OPENAI_GPT_56_MODELS.map((model) => model.id));
42
45
  export function getBuiltInOpenAiModels() {
43
46
  return getBuiltInProviderModels(OPENAI_PROVIDER_ID);
@@ -69,6 +72,10 @@ export function buildOpenAiCodexGpt56Models(baseModels = getBuiltInOpenAiCodexMo
69
72
  }
70
73
  export function buildOpenAiCodexSupplementalModels(baseModels = getBuiltInOpenAiCodexModels()) {
71
74
  const models = buildOpenAiCodexGpt56Models(baseModels);
75
+ const reserveBase = models.find((model) => model.id === OPENAI_CODEX_RESERVE_BASE_MODEL_ID);
76
+ if (reserveBase && !models.some((model) => model.id === OPENAI_CODEX_RESERVE_MODEL.id)) {
77
+ models.push({ ...cloneModel(reserveBase), ...OPENAI_CODEX_RESERVE_MODEL });
78
+ }
72
79
  if (models.some((model) => model.id === OPENAI_CODEX_GPT_6_ASTRA_MODEL.id))
73
80
  return models;
74
81
  return [...models, openAiCodexAstraModelToRegistryModel()];
@@ -76,7 +83,7 @@ export function buildOpenAiCodexSupplementalModels(baseModels = getBuiltInOpenAi
76
83
  export function registerOpenAiSupplementalModels(modelRegistry, options = {}) {
77
84
  const baseOpenAiModels = options.baseOpenAiModels ?? getBuiltInOpenAiModels();
78
85
  const openAiModels = buildOpenAiGpt56Models(baseOpenAiModels);
79
- const openAiAdded = countMissingGpt56Models(baseOpenAiModels, OPENAI_PROVIDER_ID);
86
+ const openAiAdded = countAddedModels(baseOpenAiModels, OPENAI_PROVIDER_ID, openAiModels);
80
87
  modelRegistry.registerProvider(OPENAI_PROVIDER_ID, {
81
88
  baseUrl: OPENAI_BASE_URL,
82
89
  api: OPENAI_RESPONSES_API,
@@ -85,9 +92,7 @@ export function registerOpenAiSupplementalModels(modelRegistry, options = {}) {
85
92
  });
86
93
  const baseOpenAiCodexModels = options.baseOpenAiCodexModels ?? getBuiltInOpenAiCodexModels();
87
94
  const openAiCodexModels = buildOpenAiCodexSupplementalModels(baseOpenAiCodexModels);
88
- const hasBuiltInAstra = baseOpenAiCodexModels.some((model) => model.provider === OPENAI_CODEX_PROVIDER_ID && model.id === OPENAI_CODEX_GPT_6_ASTRA_MODEL.id);
89
- const openAiCodexAdded = countMissingGpt56Models(baseOpenAiCodexModels, OPENAI_CODEX_PROVIDER_ID)
90
- + (hasBuiltInAstra ? 0 : 1);
95
+ const openAiCodexAdded = countAddedModels(baseOpenAiCodexModels, OPENAI_CODEX_PROVIDER_ID, openAiCodexModels);
91
96
  modelRegistry.registerProvider(OPENAI_CODEX_PROVIDER_ID, {
92
97
  baseUrl: OPENAI_CODEX_BASE_URL,
93
98
  api: OPENAI_CODEX_RESPONSES_API,
@@ -128,9 +133,9 @@ function buildProviderGpt56Models(options) {
128
133
  .map((model) => openAiGpt56ModelToRegistryModel(model, options));
129
134
  return [...providerBaseModels, ...additions];
130
135
  }
131
- function countMissingGpt56Models(baseModels, providerId) {
136
+ function countAddedModels(baseModels, providerId, registeredModels) {
132
137
  const existingIds = new Set(baseModels.filter((model) => model.provider === providerId).map((model) => model.id));
133
- return OPENAI_GPT_56_MODELS.filter((model) => !existingIds.has(model.id)).length;
138
+ return registeredModels.filter((model) => !existingIds.has(model.id)).length;
134
139
  }
135
140
  function cloneModel(model) {
136
141
  return {
@@ -16,13 +16,14 @@ export function buildCompactTerminalRows(traceView, options) {
16
16
  if (!traceView)
17
17
  return [];
18
18
  const turnById = mapTurnNodes(traceView.nodes);
19
+ const showToolDebugMetrics = Boolean(options.debugMode && (options.debugFeatures?.toolMetrics ?? true));
19
20
  const flatNodes = flattenTraceNodes(traceView.nodes)
20
21
  .sort((left, right) => compareTraceNodes(left.node, right.node))
21
22
  .filter((item) => item.node.type !== "agent.turn" && (options.showThinking || item.node.type !== "model.reasoning"));
22
23
  const candidates = syncThinkingToolRows(flatNodes.map((item) => createRowCandidate(item.node, item.turnId)));
23
24
  applyCompletedTurnTiming(candidates, turnById);
24
25
  const reconciled = reconcileConceptualRowCandidates(candidates);
25
- const rows = !options.debugMode && (options.toolDisplayMode ?? "default") === "default"
26
+ const rows = !showToolDebugMetrics && (options.toolDisplayMode ?? "default") === "default"
26
27
  ? groupRelatedToolCandidates(reconciled).map((candidate) => candidate.row)
27
28
  : reconciled.map((candidate) => candidate.row);
28
29
  return applyToolDisplayMode(rows, options.toolDisplayMode ?? "default");
@@ -41,14 +42,7 @@ function applyToolDisplayMode(rows, mode) {
41
42
  const slimRow = {
42
43
  ...row,
43
44
  lines: row.lines.slice(0, 1),
44
- input: undefined,
45
- output: undefined,
46
- error: undefined,
47
- markdown: undefined,
48
- expandable: false,
49
45
  singleLine: true,
50
- previewOmission: undefined,
51
- detailItems: undefined,
52
46
  };
53
47
  if (mode !== "intent")
54
48
  return [slimRow];
@@ -60,6 +54,13 @@ function applyToolDisplayMode(rows, mode) {
60
54
  prefix: "bullet",
61
55
  tokens: [token(intent, row.status === "error" ? "red" : row.status === "done" ? "green" : "cyan", "semibold")],
62
56
  }],
57
+ input: undefined,
58
+ output: undefined,
59
+ error: undefined,
60
+ markdown: undefined,
61
+ expandable: false,
62
+ previewOmission: undefined,
63
+ detailItems: undefined,
63
64
  }];
64
65
  });
65
66
  }
@@ -159,18 +160,34 @@ function createRowCandidate(node, turnId) {
159
160
  };
160
161
  break;
161
162
  }
163
+ const isToolCall = node.type === "tool.call" || node.type === "tool.result" || (node.type === "agent.delegation" && Boolean(node.toolCallId));
164
+ const reference = isToolCall ? toolCallReference(node) : undefined;
162
165
  return {
163
166
  ...candidate,
164
167
  row: {
165
168
  ...candidate.row,
166
169
  id: compactTerminalRowIdentity(node),
167
170
  intent: node.intent,
168
- isToolCall: node.type === "tool.call" || node.type === "tool.result" || (node.type === "agent.delegation" && Boolean(node.toolCallId)),
171
+ isToolCall,
172
+ toolCallReference: reference,
173
+ expandable: reference || Object.values(node.payloadRefs ?? {}).some(Boolean) ? true : candidate.row.expandable,
169
174
  toolMetrics: node.toolMetrics,
175
+ modelInferences: node.modelInferences,
170
176
  ...debugFields(node),
171
177
  },
172
178
  };
173
179
  }
180
+ function toolCallReference(node) {
181
+ if (!node.toolCallId)
182
+ return undefined;
183
+ const parsed = parseTraceToolNodeIdentity(node.id);
184
+ return {
185
+ traceNodeId: node.id,
186
+ toolCallId: node.toolCallId,
187
+ eventId: node.eventId ?? parsed?.qualifier?.eventId,
188
+ invocationOrdinal: node.toolInvocationOrdinal ?? parsed?.qualifier?.invocationOrdinal,
189
+ };
190
+ }
174
191
  export function compactTerminalRowIdentity(node) {
175
192
  if (node.type === "tool.call" || node.type === "tool.result" || node.type === "agent.delegation") {
176
193
  if (node.toolCallId) {
@@ -226,6 +243,7 @@ function reconcileConceptualRowCandidates(candidates) {
226
243
  output: candidate.row.output ?? existing.row.output,
227
244
  error: candidate.row.error ?? existing.row.error,
228
245
  payloadRefs: { ...existing.row.payloadRefs, ...candidate.row.payloadRefs },
246
+ modelInferences: mergeModelInferences(existing.row.modelInferences, candidate.row.modelInferences),
229
247
  imagePreviews: mergeImagePreviews(existing.row.imagePreviews, candidate.row.imagePreviews),
230
248
  },
231
249
  };
@@ -310,6 +328,16 @@ function assistantPartIndex(rowId) {
310
328
  const index = Number(match[1]);
311
329
  return Number.isSafeInteger(index) ? index : undefined;
312
330
  }
331
+ function mergeModelInferences(existing, candidate) {
332
+ if (!existing?.length)
333
+ return candidate;
334
+ if (!candidate?.length)
335
+ return existing;
336
+ const merged = new Map(existing.map((record) => [record.id, record]));
337
+ for (const record of candidate)
338
+ merged.set(record.id, record);
339
+ return [...merged.values()];
340
+ }
313
341
  function mergeImagePreviews(existing, candidate) {
314
342
  if (!existing?.length)
315
343
  return candidate?.slice(0, MAX_COMPACT_TERMINAL_IMAGE_PREVIEWS);
@@ -329,6 +357,9 @@ function applyCompletedTurnTiming(candidates, turnById) {
329
357
  continue;
330
358
  const turnCandidates = candidates.filter((candidate) => candidate.turnId === turn.id);
331
359
  const finalCandidate = turnCandidates.at(-1);
360
+ if (finalCandidate && turn.modelInferences?.length) {
361
+ finalCandidate.row.modelInferences = mergeModelInferences(finalCandidate.row.modelInferences, turn.modelInferences);
362
+ }
332
363
  if (finalCandidate?.row.kind !== "message.assistant" || finalCandidate.row.status === "running")
333
364
  continue;
334
365
  finalCandidate.row.startedAt = turn.startedAt;
@@ -362,7 +393,8 @@ function createUserMessageRow(node) {
362
393
  lines: [{ prefix: "prompt", tokens: [token(text)] }],
363
394
  sourceNodeIds: [node.id],
364
395
  forkEntryId: node.entryId,
365
- pendingMessageDelivery: pendingUserMessageDelivery(node),
396
+ pendingMessageDelivery: pendingUserMessageDelivery(node) ?? (node.status === "running" && node.messageDeliveryState ? "queue" : undefined),
397
+ messageDeliveryState: node.messageDeliveryState,
366
398
  startedAt: node.startedAt,
367
399
  output: text,
368
400
  payloadRefs: node.payloadRefs,
@@ -657,9 +689,16 @@ function createCompactionRow(node) {
657
689
  input: node.input,
658
690
  output: node.output,
659
691
  error: node.error,
660
- expandable: node.input !== undefined || node.output !== undefined || Boolean(node.error),
692
+ compactionStats: node.compactionStats,
693
+ compactionMarkdown: compactionMarkdown(node.output),
694
+ expandable: node.status === "error" && (node.input !== undefined || node.output !== undefined || Boolean(node.error)),
661
695
  };
662
696
  }
697
+ function compactionMarkdown(value) {
698
+ if (!isRecord(value))
699
+ return undefined;
700
+ return stringValue(value.summary);
701
+ }
663
702
  function createExecutionCommandRow(node) {
664
703
  if (node.title === "status") {
665
704
  return createStatusToolRow(node);
@@ -966,13 +1005,14 @@ function createExploringGroup(candidates) {
966
1005
  }] : []),
967
1006
  ],
968
1007
  sourceNodeIds: candidates.flatMap((candidate) => candidate.row.sourceNodeIds),
1008
+ modelInferences: candidates.reduce((records, candidate) => mergeModelInferences(records, candidate.row.modelInferences), undefined),
969
1009
  eventId: firstRow?.eventId,
970
1010
  runId: firstRow?.runId,
971
1011
  orderSource: firstRow?.orderSource,
972
1012
  orderStreamId: firstRow?.orderStreamId,
973
1013
  orderStreamFrameIndex: firstRow?.orderStreamFrameIndex,
974
1014
  detailItems,
975
- expandable: detailItems.some((item) => item.input !== undefined || item.output !== undefined || Boolean(item.error)),
1015
+ expandable: detailItems.some((item) => item.toolCallReference || item.input !== undefined || item.output !== undefined || Boolean(item.error)),
976
1016
  previewOmission: omittedDetailCount > 0 ? {
977
1017
  source: "details",
978
1018
  visibleLineCount: visibleDetailItems.length,
@@ -1014,13 +1054,14 @@ function createImageGroup(candidates) {
1014
1054
  }] : []),
1015
1055
  ],
1016
1056
  sourceNodeIds: candidates.flatMap((candidate) => candidate.row.sourceNodeIds),
1057
+ modelInferences: candidates.reduce((records, candidate) => mergeModelInferences(records, candidate.row.modelInferences), undefined),
1017
1058
  eventId: firstRow?.eventId,
1018
1059
  runId: firstRow?.runId,
1019
1060
  orderSource: firstRow?.orderSource,
1020
1061
  orderStreamId: firstRow?.orderStreamId,
1021
1062
  orderStreamFrameIndex: firstRow?.orderStreamFrameIndex,
1022
1063
  detailItems,
1023
- expandable: detailItems.some((item) => item.input !== undefined || item.output !== undefined || Boolean(item.error)),
1064
+ expandable: detailItems.some((item) => item.toolCallReference || item.input !== undefined || item.output !== undefined || Boolean(item.error)),
1024
1065
  imagePreviews: detailItems.flatMap((item) => item.imagePreviews ?? []).slice(0, MAX_COMPACT_TERMINAL_IMAGE_PREVIEWS),
1025
1066
  previewOmission: omittedDetailCount > 0 ? {
1026
1067
  source: "details",
@@ -1043,6 +1084,7 @@ function detailItemsForGroup(candidates, kind) {
1043
1084
  error: candidate.row.error,
1044
1085
  payloadRefs: candidate.row.payloadRefs,
1045
1086
  linkedPiboSessionId: candidate.row.linkedPiboSessionId,
1087
+ toolCallReference: candidate.row.toolCallReference,
1046
1088
  previewOmission: candidate.row.previewOmission,
1047
1089
  imagePreviews: candidate.row.imagePreviews,
1048
1090
  };
@@ -1,4 +1,4 @@
1
- import { ChatDataIngestService } from "../data/ingest-service.js";
1
+ import { ChatDataIngestService, outputIdempotencyKey } from "../data/ingest-service.js";
2
2
  import { PiboDataStore } from "../data/pibo-store.js";
3
3
  import { PIBO_AGENT_OBSERVATION_AUTO_CURSOR_MAX_SCOPES, createPiboSession, matchesFindInput, } from "./store.js";
4
4
  import { createLegacyPiRuntimeSessionBinding, nextRuntimeSessionBinding, RuntimeSessionBindingConflictError, } from "./runtime-binding.js";
@@ -49,6 +49,7 @@ export class PiboDataSessionStore {
49
49
  const row = this.db.prepare(`${SESSION_SELECT} WHERE s.id = ? AND s.deleted_at IS NULL`).get(id);
50
50
  return row ? sessionFromRow(row) : undefined;
51
51
  }
52
+ getStructureRevision() { return this.db.prepare("SELECT revision FROM chat_navigation_clock WHERE id=1").get().revision; }
52
53
  list() {
53
54
  return this.db.prepare(`${SESSION_SELECT} WHERE s.deleted_at IS NULL ORDER BY s.updated_at DESC`).all().map(sessionFromRow);
54
55
  }
@@ -469,9 +470,26 @@ export class PiboDataSessionStore {
469
470
  const at = input.at ?? new Date().toISOString();
470
471
  const runsBySession = groupRunsByController(input.recoveredRuns ?? []);
471
472
  return this.dataStore.transaction(() => {
473
+ // Durable terminal output wins over stale telemetry. Do not send a different
474
+ // recovery payload through the same output identity during startup.
475
+ const terminalErrors = new Map();
472
476
  const recoveredTurns = this.dataStore.telemetry.recoverInterruptedTurns({
473
477
  at,
474
- resolveOutcome: (turn) => recoveryOutcomeForTurn(turn, runsBySession.get(turn.piboSessionId) ?? []),
478
+ resolveOutcome: (turn) => {
479
+ const identity = { type: "session_error", piboSessionId: turn.piboSessionId, eventId: recoveryEventId(turn), error: "" };
480
+ const existing = this.dataStore.eventLog.findByIdempotencyKey(outputIdempotencyKey(identity));
481
+ if (existing) {
482
+ const event = {
483
+ ...identity,
484
+ error: typeof existing.attributes.error === "string" ? existing.attributes.error : existing.previewText ?? "Runtime error",
485
+ errorDetails: existing.attributes.errorDetails,
486
+ };
487
+ terminalErrors.set(turn.turnId, event);
488
+ const details = event.errorDetails;
489
+ return { status: details?.code === "timeout" ? "timeout" : details?.errorClass === "runtime_abort" ? "aborted" : "error", summary: event.error };
490
+ }
491
+ return recoveryOutcomeForTurn(turn, runsBySession.get(turn.piboSessionId) ?? []);
492
+ },
475
493
  });
476
494
  if (recoveredTurns.length === 0)
477
495
  return [];
@@ -482,7 +500,8 @@ export class PiboDataSessionStore {
482
500
  if (!session)
483
501
  continue;
484
502
  const row = this.db.prepare("SELECT room_id FROM sessions WHERE id = ?").get(session.id);
485
- const event = {
503
+ const existingError = terminalErrors.get(recovered.turn.turnId);
504
+ const event = existingError ?? {
486
505
  type: "session_error",
487
506
  piboSessionId: session.id,
488
507
  eventId: recoveryEventId(recovered.turn),
@@ -512,13 +531,14 @@ export class PiboDataSessionStore {
512
531
  updated_at = ?
513
532
  WHERE session_id = ?
514
533
  `).run(at, at, at, session.id);
515
- ingest.ingestOutputEvent({
516
- session,
517
- roomId: row?.room_id ?? undefined,
518
- actorId: session.id,
519
- event,
520
- createdAt: at,
521
- });
534
+ if (!existingError)
535
+ ingest.ingestOutputEvent({
536
+ session,
537
+ roomId: row?.room_id ?? undefined,
538
+ actorId: session.id,
539
+ event,
540
+ createdAt: at,
541
+ });
522
542
  results.push({
523
543
  turnId: recovered.turn.turnId,
524
544
  piboSessionId: session.id,
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_DEBUG_FEATURE_SETTINGS = {
2
+ toolMetrics: true,
3
+ modelInferenceMetrics: true,
4
+ };