@pasko70/pibo 3.4.4 → 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 (79) hide show
  1. package/dist/agent-runtime/routed-session.js +37 -12
  2. package/dist/agent-runtimes/pi/adapter.js +1 -0
  3. package/dist/agent-runtimes/pi/routed-session.js +2 -2
  4. package/dist/apps/chat/bounded-event-stream.js +98 -0
  5. package/dist/apps/chat/chat-settings-routes.js +3 -3
  6. package/dist/apps/chat/data/chat-data-mappers.js +32 -13
  7. package/dist/apps/chat/data/event-command-service.js +30 -21
  8. package/dist/apps/chat/data/history-query-service.js +34 -25
  9. package/dist/apps/chat/data/read-state-service.js +13 -0
  10. package/dist/apps/chat/data/session-query-service.js +19 -11
  11. package/dist/apps/chat/data/timeline-query-service.js +19 -7
  12. package/dist/apps/chat/message-command-dispatcher.js +134 -0
  13. package/dist/apps/chat/output-compactor.js +9 -0
  14. package/dist/apps/chat/output-event-policy.js +9 -1
  15. package/dist/apps/chat/stream.js +21 -3
  16. package/dist/apps/chat/telemetry-retention-service.js +113 -8
  17. package/dist/apps/chat/trace-response-cache.js +46 -0
  18. package/dist/apps/chat/trace-v2.js +12 -6
  19. package/dist/apps/chat/trace.js +1 -1
  20. package/dist/apps/chat/web-app.js +608 -281
  21. package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-BG0n7zLd.js} +1 -1
  22. package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-D6TjFhAm.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-D79vyxSX.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-DFZ8cwh0.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-cOjokPrK.js} +1 -1
  26. package/dist/apps/chat-ui/assets/index-RMHUTJ62.js +229 -0
  27. package/dist/apps/chat-ui/assets/{index-BOceJ0jM.css → index-hEkrlRk-.css} +1 -1
  28. package/dist/apps/chat-ui/index.html +2 -2
  29. package/dist/apps/chat-vscode-web/assets/index-xacbCyTx.js +44 -0
  30. package/dist/apps/chat-vscode-web/index.html +1 -1
  31. package/dist/compute/pool/seeds.js +25 -3
  32. package/dist/core/events.js +4 -0
  33. package/dist/core/output-render-sequence.js +2 -0
  34. package/dist/core/provider-capacity.js +33 -0
  35. package/dist/core/provider-telemetry.js +21 -6
  36. package/dist/core/runtime-capacity.js +174 -0
  37. package/dist/core/runtime-telemetry.js +40 -12
  38. package/dist/core/session-router.js +79 -2
  39. package/dist/data/async-chat-reads.js +38 -0
  40. package/dist/data/async-chat-storage.js +91 -0
  41. package/dist/data/async-telemetry-maintenance.js +9 -0
  42. package/dist/data/bounded-worker-client.js +256 -0
  43. package/dist/data/chat-read-projections.js +159 -0
  44. package/dist/data/chat-read-worker.js +73 -0
  45. package/dist/data/chat-storage-worker.js +146 -0
  46. package/dist/data/ingest-service.js +79 -14
  47. package/dist/data/message-command-store.js +148 -0
  48. package/dist/data/payload-store.js +92 -11
  49. package/dist/data/pibo-store.js +10 -8
  50. package/dist/data/schema.js +16 -2
  51. package/dist/data/session-store.js +3 -1
  52. package/dist/data/storage-backup.js +278 -0
  53. package/dist/data/telemetry-capture.js +188 -0
  54. package/dist/data/telemetry-command.js +3 -0
  55. package/dist/data/telemetry-maintenance-worker.js +40 -0
  56. package/dist/data/telemetry-maintenance.js +110 -0
  57. package/dist/data/telemetry-retention.js +16 -7
  58. package/dist/data/telemetry-worker.js +111 -0
  59. package/dist/data/telemetry-writer.js +150 -83
  60. package/dist/data/telemetry.js +5 -0
  61. package/dist/debug/index.js +52 -0
  62. package/dist/debug/storage-backup.js +33 -0
  63. package/dist/debug/telemetry-capture.js +66 -0
  64. package/dist/gateway/server.js +2 -0
  65. package/dist/providers/openai-gpt56.js +11 -6
  66. package/dist/session-ui/terminalRows.js +48 -6
  67. package/dist/sessions/pibo-data-store.js +1 -0
  68. package/dist/shared/debug-features.js +4 -0
  69. package/dist/shared/model-inference-metrics.js +23 -0
  70. package/dist/shared/trace-event-projection.js +59 -2
  71. package/dist/shared/trace-history.js +9 -0
  72. package/dist/shared/trace-live-reducer.js +1 -0
  73. package/dist/shared/trace-patch-nodes.js +19 -0
  74. package/dist/web/channel.js +3 -0
  75. package/dist/web/http.js +36 -3
  76. package/npm-shrinkwrap.json +2 -2
  77. package/package.json +1 -1
  78. package/dist/apps/chat-ui/assets/index-BOemYq-V.js +0 -228
  79. package/dist/apps/chat-vscode-web/assets/index-CMwTB8o8.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
+ }
@@ -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");
@@ -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
  };
@@ -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
  }
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_DEBUG_FEATURE_SETTINGS = {
2
+ toolMetrics: true,
3
+ modelInferenceMetrics: true,
4
+ };
@@ -0,0 +1,23 @@
1
+ export function modelInferenceInputTokens(metrics) {
2
+ const total = tokenCount(metrics?.totalTokens);
3
+ const output = tokenCount(metrics?.outputTokens);
4
+ if (total !== undefined && output !== undefined)
5
+ return Math.max(0, total - output);
6
+ return tokenCount(metrics?.inputTokens);
7
+ }
8
+ export function modelInferenceCachedInputTokens(metrics) {
9
+ const cacheRead = tokenCount(metrics?.cacheReadTokens);
10
+ const cacheWrite = tokenCount(metrics?.cacheWriteTokens);
11
+ if (cacheRead === undefined && cacheWrite === undefined)
12
+ return undefined;
13
+ return (cacheRead ?? 0) + (cacheWrite ?? 0);
14
+ }
15
+ export function modelInferenceUncachedInputTokens(metrics) {
16
+ const input = modelInferenceInputTokens(metrics);
17
+ if (input === undefined)
18
+ return undefined;
19
+ return Math.max(0, input - (modelInferenceCachedInputTokens(metrics) ?? 0));
20
+ }
21
+ function tokenCount(value) {
22
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined;
23
+ }
@@ -6,6 +6,10 @@ import { findLikelyTraceChildSession, isSubagentToolName, } from "./trace-subage
6
6
  import { qualifiedToolNodeId } from "./trace-tool-identity.js";
7
7
  export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent, childByParent, linkedChildByToolCallId, historyCoverage, openTranscriptEventIds, sessionStatus) {
8
8
  const payload = storedEvent.payload;
9
+ if (payload.type === "assistant_usage") {
10
+ attachModelInferenceToLatestOutput(nodes, byId, payload, storedEvent);
11
+ return;
12
+ }
9
13
  if (payload.type === "message_started" && payload.source === "user" && payload.eventId) {
10
14
  const persistedUser = flattenTraceNodes([...nodes]).find((node) => node.type === "user.message"
11
15
  && isPersistedHistorySource(node.source)
@@ -42,6 +46,7 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
42
46
  }
43
47
  if (payload.type === "assistant_message") {
44
48
  const node = assistantMessageNodeFromEvent(piboSessionId, payload, storedEvent.createdAt, storedEvent.eventSequence, storedEvent.streamId, storedEvent.streamFrameIndex, storedEvent.traceSource, storedEvent.id);
49
+ applyStoredPayloadRef(node, payload, storedEvent.storedPayloadRef);
45
50
  const existing = byId.get(node.id) ?? findMatchingContentNode(byId, node);
46
51
  if (existing) {
47
52
  mergeAssistantMessageEvent(existing, node);
@@ -153,8 +158,12 @@ function applyRenderSequenceToHistoryNode(byId, storedEvent, event) {
153
158
  && typeof event.toolCallId === "string"
154
159
  && node.toolCallId === event.toolCallId
155
160
  && (!eventId || !node.eventId || node.eventId === eventId)));
156
- if (target)
157
- applyRenderSequence(target, storedEvent, event);
161
+ if (!target)
162
+ return;
163
+ applyRenderSequence(target, storedEvent, event);
164
+ if (event.type === "tool_execution_finished" && event.toolMetrics) {
165
+ target.toolMetrics = event.toolMetrics;
166
+ }
158
167
  }
159
168
  function applyRenderSequence(node, storedEvent, event) {
160
169
  const renderSequence = storedEvent.renderSequence ?? event.renderSequence;
@@ -171,6 +180,9 @@ function applyRenderSequence(node, storedEvent, event) {
171
180
  function applyStoredPayloadRef(node, event, storedPayloadRef) {
172
181
  if (!storedPayloadRef)
173
182
  return;
183
+ if (event.type === "assistant_message" || event.type === "thinking_finished") {
184
+ node.payloadRefs = { ...node.payloadRefs, [storedPayloadRef.payloadKind ?? "output"]: storedPayloadRef };
185
+ }
174
186
  if (event.type === "tool_call" || event.type === "tool_execution_started") {
175
187
  node.payloadRefs = { ...node.payloadRefs, input: storedPayloadRef };
176
188
  }
@@ -524,6 +536,47 @@ export function latestTraceStreamId(events, initial) {
524
536
  }
525
537
  return latest;
526
538
  }
539
+ function attachModelInferenceToLatestOutput(nodes, byId, event, storedEvent) {
540
+ const eventId = event.eventId;
541
+ const candidates = flattenTraceNodes(nodes)
542
+ .filter((node) => node.eventId === eventId && traceNodeStartedBeforeInference(node, storedEvent) && (node.type === "assistant.message"
543
+ || node.type === "model.reasoning"
544
+ || node.type === "tool.call"
545
+ || node.type === "agent.delegation"))
546
+ .sort(compareTraceNodes);
547
+ const target = candidates.at(-1) ?? (eventId ? byId.get(messageTurnNodeId(eventId)) : undefined);
548
+ if (!target)
549
+ return;
550
+ const id = eventId ? `${eventId}:usage:${event.usageIndex ?? 0}` : storedEvent.id;
551
+ const record = {
552
+ id,
553
+ completedAt: storedEvent.createdAt,
554
+ metrics: {
555
+ ...(event.inputTokens === undefined ? {} : { inputTokens: event.inputTokens }),
556
+ ...(event.outputTokens === undefined ? {} : { outputTokens: event.outputTokens }),
557
+ ...(event.cacheReadTokens === undefined ? {} : { cacheReadTokens: event.cacheReadTokens }),
558
+ ...(event.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: event.cacheWriteTokens }),
559
+ ...(event.reasoningTokens === undefined ? {} : { reasoningTokens: event.reasoningTokens }),
560
+ totalTokens: event.totalTokens,
561
+ ...(event.costUsd === undefined ? {} : { costUsd: event.costUsd }),
562
+ },
563
+ };
564
+ target.modelInferences = [...(target.modelInferences ?? []).filter((item) => item.id !== id), record];
565
+ }
566
+ function traceNodeStartedBeforeInference(node, storedEvent) {
567
+ const inferenceRenderSequence = storedEvent.renderSequence;
568
+ const nodeRenderSequence = node.orderKey?.renderSequence;
569
+ if (inferenceRenderSequence !== undefined && nodeRenderSequence !== undefined) {
570
+ return nodeRenderSequence <= inferenceRenderSequence;
571
+ }
572
+ const inferenceSequence = storedEvent.eventSequence ?? storedEvent.streamId;
573
+ const nodeSequence = node.orderKey?.eventSequence ?? node.orderKey?.streamId;
574
+ if (inferenceSequence !== undefined && nodeSequence !== undefined)
575
+ return nodeSequence <= inferenceSequence;
576
+ const inferenceTime = Date.parse(storedEvent.createdAt);
577
+ const nodeTime = Date.parse(node.startedAt ?? node.completedAt ?? "");
578
+ return !Number.isFinite(inferenceTime) || !Number.isFinite(nodeTime) || nodeTime <= inferenceTime;
579
+ }
527
580
  // ── event → node helpers ─────────────────────────────────────────
528
581
  function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByToolCallId, sessionStatus, createdAt, eventSequence, streamId, streamFrameIndex, traceSource, storedInstanceId) {
529
582
  const eventId = "eventId" in event && typeof event.eventId === "string" ? event.eventId : undefined;
@@ -562,6 +615,7 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
562
615
  type: "user.message",
563
616
  title: "User Message",
564
617
  status: isOptimisticUserMessageEvent(event) ? "running" : "done",
618
+ messageDeliveryState: isOptimisticUserMessageEvent(event) ? "sending" : undefined,
565
619
  summary: event.text,
566
620
  output: event.text,
567
621
  };
@@ -711,6 +765,7 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
711
765
  input: { reason: event.reason },
712
766
  output: event.result,
713
767
  error: event.errorMessage,
768
+ compactionStats: event.compactionStats,
714
769
  stableKey: `compaction:${eventInstanceKey}`,
715
770
  };
716
771
  }
@@ -768,6 +823,7 @@ function findMatchingContentNode(byId, update) {
768
823
  return [...byId.values()].find((candidate) => candidate.type === update.type && candidate.stableKey === update.stableKey);
769
824
  }
770
825
  function mergeAssistantMessageEvent(target, update) {
826
+ target.payloadRefs = { ...target.payloadRefs, ...update.payloadRefs };
771
827
  target.status = update.status;
772
828
  target.summary = update.summary ?? target.summary;
773
829
  target.output = update.output ?? target.output;
@@ -1163,6 +1219,7 @@ function mergeCompactionEvent(target, update) {
1163
1219
  target.input = update.input ?? target.input;
1164
1220
  target.output = update.output ?? target.output;
1165
1221
  target.error = update.error ?? target.error;
1222
+ target.compactionStats = update.compactionStats ?? target.compactionStats;
1166
1223
  target.completedAt = update.completedAt ?? target.completedAt;
1167
1224
  }
1168
1225
  function hasVisibleText(value) {
@@ -710,6 +710,7 @@ function createUserMessageNode(piboSessionId, entry, entryIndex, timing, fallbac
710
710
  startedAt: entry.createdAt,
711
711
  summary: text,
712
712
  output: text,
713
+ payloadRefs: historyContentPayloadRef(entry),
713
714
  error: entry.error,
714
715
  source,
715
716
  stableKey: eventIdentity ?? historyEntryStableKey(entry),
@@ -902,6 +903,7 @@ function createAssistantMessageNode(input) {
902
903
  completedAt: input.completedAt,
903
904
  summary: input.text,
904
905
  output: input.text,
906
+ payloadRefs: historyContentPayloadRef(input.entry),
905
907
  error: input.error,
906
908
  source,
907
909
  stableKey: eventIdentity ? `assistant:${eventIdentity}` : `${historyEntryStableKey(input.entry)}:response:${input.contentPartIndex}`,
@@ -909,6 +911,13 @@ function createAssistantMessageNode(input) {
909
911
  children: input.children ?? [],
910
912
  };
911
913
  }
914
+ /** Preserve a stored full body when the bounded history page contains its preview. */
915
+ function historyContentPayloadRef(entry) {
916
+ const ref = entry.source === "product" ? entry.metadata?.tracePayloadRef : undefined;
917
+ if (!ref || typeof ref !== "object" || Array.isArray(ref) || typeof ref.ref !== "string" || typeof ref.byteLength !== "number" || typeof ref.preview !== "string")
918
+ return undefined;
919
+ return { output: ref };
920
+ }
912
921
  function historyMessageParts(entry) {
913
922
  return typeof entry.content === "string" ? [{ type: "text", text: entry.content }] : [...entry.content];
914
923
  }
@@ -165,6 +165,7 @@ function makeStored(streamEvent, piboSessionId, type, payload, nextSequence, now
165
165
  streamId: typeof streamEvent.streamId === "number" ? streamEvent.streamId : undefined,
166
166
  streamFrameIndex,
167
167
  traceSource: "live",
168
+ storedPayloadRef: streamEvent.storedPayloadRef,
168
169
  eventId: typeof positionedPayload.eventId === "string" ? positionedPayload.eventId : undefined,
169
170
  type,
170
171
  createdAt: streamEventCreatedAt(streamEvent) ?? now(),
@@ -64,6 +64,7 @@ function traceNodeShallowEqual(left, right) {
64
64
  left.toolMetrics?.inputTokens === right.toolMetrics?.inputTokens &&
65
65
  left.toolMetrics?.outputTokens === right.toolMetrics?.outputTokens &&
66
66
  left.toolMetrics?.tokenBasis === right.toolMetrics?.tokenBasis &&
67
+ modelInferenceRecordsEqual(left.modelInferences, right.modelInferences) &&
67
68
  left.summary === right.summary &&
68
69
  left.input === right.input &&
69
70
  left.output === right.output &&
@@ -73,6 +74,24 @@ function traceNodeShallowEqual(left, right) {
73
74
  left.stableKey === right.stableKey &&
74
75
  traceOrderKeyEqual(left.orderKey, right.orderKey));
75
76
  }
77
+ function modelInferenceRecordsEqual(left, right) {
78
+ if (left === right)
79
+ return true;
80
+ if (!left || !right || left.length !== right.length)
81
+ return false;
82
+ return left.every((item, index) => {
83
+ const other = right[index];
84
+ return item.id === other?.id
85
+ && item.completedAt === other.completedAt
86
+ && item.metrics.inputTokens === other.metrics.inputTokens
87
+ && item.metrics.outputTokens === other.metrics.outputTokens
88
+ && item.metrics.cacheReadTokens === other.metrics.cacheReadTokens
89
+ && item.metrics.cacheWriteTokens === other.metrics.cacheWriteTokens
90
+ && item.metrics.reasoningTokens === other.metrics.reasoningTokens
91
+ && item.metrics.totalTokens === other.metrics.totalTokens
92
+ && item.metrics.costUsd === other.metrics.costUsd;
93
+ });
94
+ }
76
95
  function traceOrderKeyEqual(left, right) {
77
96
  if (left === right)
78
97
  return true;
@@ -199,6 +199,7 @@ function createGatewayStatusResponse(channelContext, options, generation) {
199
199
  generation,
200
200
  health: { status: "ok", mode },
201
201
  runtimeStatuses: createGatewayRuntimeStatuses(channelContext),
202
+ ...(channelContext.getRuntimeCapacityStatus ? { runtimeCapacity: channelContext.getRuntimeCapacityStatus() } : {}),
202
203
  activeRuns: collectActiveRuns(channelContext),
203
204
  });
204
205
  }
@@ -401,6 +402,8 @@ export function createWebHostChannel(options = {}) {
401
402
  return;
402
403
  shuttingDown = false;
403
404
  context = channelContext;
405
+ for (const app of channelContext.getWebApps())
406
+ await app.initialize?.(createAppContext(channelContext));
404
407
  server = createServer((request, response) => {
405
408
  void handleRequest(request, response);
406
409
  });
package/dist/web/http.js CHANGED
@@ -107,11 +107,11 @@ export async function sendWebResponse(response, webResponse, options = {}) {
107
107
  }
108
108
  const reader = webResponse.body.getReader();
109
109
  const cancel = () => {
110
- void reader.cancel();
110
+ void reader.cancel().catch(() => undefined);
111
111
  };
112
112
  const abort = () => {
113
113
  const socket = response.socket;
114
- void reader.cancel();
114
+ void reader.cancel().catch(() => undefined);
115
115
  if (!response.writableEnded)
116
116
  response.end();
117
117
  socket?.end();
@@ -125,7 +125,20 @@ export async function sendWebResponse(response, webResponse, options = {}) {
125
125
  const { done, value } = await reader.read();
126
126
  if (done)
127
127
  break;
128
- response.write(Buffer.from(value));
128
+ const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
129
+ for (let offset = 0; offset < bytes.length; offset += 64 * 1024) {
130
+ if (response.destroyed || response.writableEnded)
131
+ return;
132
+ if (!response.write(bytes.subarray(offset, offset + 64 * 1024))) {
133
+ const drained = await waitForResponseDrain(response, (webResponse.headers.get("content-type") ?? "").startsWith("text/event-stream") ? 5000 : undefined, options.signal);
134
+ if (!drained) {
135
+ void reader.cancel().catch(() => undefined);
136
+ if (!response.destroyed)
137
+ response.destroy();
138
+ return;
139
+ }
140
+ }
141
+ }
129
142
  }
130
143
  if (!response.writableEnded)
131
144
  response.end();
@@ -206,3 +219,23 @@ function appendVary(existing, value) {
206
219
  function appendServerTiming(existing, value) {
207
220
  return existing ? `${existing}, ${value}` : value;
208
221
  }
222
+ /** Honor socket pressure before pulling another application frame. */
223
+ function waitForResponseDrain(response, maximumAgeMs, signal) {
224
+ if (response.destroyed || response.writableEnded || signal?.aborted)
225
+ return Promise.resolve(false);
226
+ return new Promise(resolve => {
227
+ let timer;
228
+ const finish = (ready) => { if (timer)
229
+ clearTimeout(timer); response.off("drain", drain); response.off("close", close); response.off("error", close); signal?.removeEventListener("abort", close); resolve(ready); };
230
+ const drain = () => finish(true);
231
+ const close = () => finish(false);
232
+ response.once("drain", drain);
233
+ response.once("close", close);
234
+ response.once("error", close);
235
+ signal?.addEventListener("abort", close, { once: true });
236
+ if (maximumAgeMs !== undefined) {
237
+ timer = setTimeout(close, maximumAgeMs);
238
+ timer.unref();
239
+ }
240
+ });
241
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.4.4",
3
+ "version": "3.5.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "3.4.4",
9
+ "version": "3.5.0",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],