@pasko70/pibo 3.4.4 → 3.5.1

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 (91) hide show
  1. package/dist/agent-runtime/routed-session.js +111 -25
  2. package/dist/agent-runtimes/codex-native/turn.js +27 -3
  3. package/dist/agent-runtimes/pi/adapter.js +1 -0
  4. package/dist/agent-runtimes/pi/routed-session.js +2 -2
  5. package/dist/apps/chat/bounded-event-stream.js +98 -0
  6. package/dist/apps/chat/chat-settings-routes.js +3 -3
  7. package/dist/apps/chat/data/chat-data-mappers.js +40 -14
  8. package/dist/apps/chat/data/event-command-service.js +30 -21
  9. package/dist/apps/chat/data/history-query-service.js +34 -25
  10. package/dist/apps/chat/data/read-state-service.js +37 -3
  11. package/dist/apps/chat/data/session-query-service.js +19 -11
  12. package/dist/apps/chat/data/timeline-query-service.js +19 -7
  13. package/dist/apps/chat/message-command-dispatcher.js +149 -0
  14. package/dist/apps/chat/output-compactor.js +9 -0
  15. package/dist/apps/chat/output-event-policy.js +9 -1
  16. package/dist/apps/chat/stream.js +21 -3
  17. package/dist/apps/chat/telemetry-retention-service.js +113 -8
  18. package/dist/apps/chat/trace-response-cache.js +46 -0
  19. package/dist/apps/chat/trace-v2.js +12 -6
  20. package/dist/apps/chat/trace.js +1 -1
  21. package/dist/apps/chat/web-app.js +645 -297
  22. package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-B-auLrzD.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-BA_dsINH.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-eJZar_0-.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-wNNR2Bci.js} +1 -1
  26. package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-zcmEsIEp.js} +1 -1
  27. package/dist/apps/chat-ui/assets/index-DEkbN5Vo.js +229 -0
  28. package/dist/apps/chat-ui/assets/index-DZK6Tzil.css +1 -0
  29. package/dist/apps/chat-ui/index.html +2 -2
  30. package/dist/apps/chat-vscode-web/assets/index-DZgW1fCB.js +44 -0
  31. package/dist/apps/chat-vscode-web/index.html +1 -1
  32. package/dist/cli-session/localSessionSource.js +3 -2
  33. package/dist/compute/pool/seeds.js +25 -3
  34. package/dist/core/events.js +4 -0
  35. package/dist/core/output-render-sequence.js +65 -6
  36. package/dist/core/provider-capacity.js +33 -0
  37. package/dist/core/provider-telemetry.js +21 -6
  38. package/dist/core/runtime-capacity.js +174 -0
  39. package/dist/core/runtime-telemetry.js +40 -12
  40. package/dist/core/session-router.js +178 -13
  41. package/dist/data/async-chat-reads.js +38 -0
  42. package/dist/data/async-chat-storage.js +96 -0
  43. package/dist/data/async-telemetry-maintenance.js +9 -0
  44. package/dist/data/bounded-worker-client.js +256 -0
  45. package/dist/data/chat-read-projections.js +159 -0
  46. package/dist/data/chat-read-worker.js +73 -0
  47. package/dist/data/chat-storage-worker.js +167 -0
  48. package/dist/data/ingest-service.js +152 -18
  49. package/dist/data/message-command-store.js +290 -0
  50. package/dist/data/payload-store.js +92 -11
  51. package/dist/data/pibo-store.js +10 -8
  52. package/dist/data/schema.js +39 -4
  53. package/dist/data/session-store.js +3 -1
  54. package/dist/data/storage-backup.js +278 -0
  55. package/dist/data/storage-maintenance.js +344 -0
  56. package/dist/data/storage-verification-worker.js +25 -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 +207 -1
  66. package/dist/debug/message-queue.js +108 -0
  67. package/dist/debug/output-collision-repair.js +140 -0
  68. package/dist/debug/output-integrity.js +38 -2
  69. package/dist/debug/output-repair.js +1 -0
  70. package/dist/debug/storage-backup.js +41 -0
  71. package/dist/debug/storage-maintenance.js +78 -0
  72. package/dist/debug/telemetry-capture.js +66 -0
  73. package/dist/gateway/cli.js +71 -7
  74. package/dist/gateway/server.js +3 -0
  75. package/dist/providers/openai-gpt56.js +11 -6
  76. package/dist/reliability/store.js +119 -23
  77. package/dist/session-ui/terminalRows.js +54 -13
  78. package/dist/sessions/pibo-data-store.js +19 -14
  79. package/dist/shared/debug-features.js +4 -0
  80. package/dist/shared/model-inference-metrics.js +23 -0
  81. package/dist/shared/trace-event-projection.js +69 -2
  82. package/dist/shared/trace-history.js +9 -0
  83. package/dist/shared/trace-live-reducer.js +1 -0
  84. package/dist/shared/trace-patch-nodes.js +19 -0
  85. package/dist/web/channel.js +117 -12
  86. package/dist/web/http.js +135 -41
  87. package/npm-shrinkwrap.json +2 -2
  88. package/package.json +1 -1
  89. package/dist/apps/chat-ui/assets/index-BOceJ0jM.css +0 -1
  90. package/dist/apps/chat-ui/assets/index-BOemYq-V.js +0 -228
  91. package/dist/apps/chat-vscode-web/assets/index-CMwTB8o8.js +0 -43
@@ -1,3 +1,4 @@
1
+ import { previouslyClearedMessages } from "../core/events.js";
1
2
  import { PiboSteeringUnavailableError, } from "../core/events.js";
2
3
  import { PIBO_PROVIDER_RECOVERY_PROMPT, isPiboProviderFallbackError } from "../core/provider-recovery.js";
3
4
  import { normalizeSessionErrorDetails, runtimeSessionErrorDetails } from "../core/session-errors.js";
@@ -17,6 +18,35 @@ const RUN_REMINDER_MAX_PROVIDER_ROUNDS = 64;
17
18
  const RUN_REMINDER_MAX_ACTIVE_TOKENS = 2_000_000;
18
19
  export const RUN_REMINDER_MAX_DURATION_MS = 15 * 60 * 1000;
19
20
  const RUN_REMINDER_MAX_REPEATED_TOOL_CALLS = 12;
21
+ const RUNTIME_QUEUE_MAX_MESSAGE_BYTES = 1024 * 1024;
22
+ const RUNTIME_QUEUE_MAX_MESSAGES = 64;
23
+ const RUNTIME_QUEUE_MAX_BYTES = 4 * 1024 * 1024;
24
+ const RUNTIME_QUEUE_MAX_WAIT_MS = 10 * 60 * 1000;
25
+ export class RuntimeQueueCapacityError extends Error {
26
+ dimension;
27
+ current;
28
+ limit;
29
+ code = "runtime_capacity_unavailable";
30
+ constructor(dimension, current, limit) {
31
+ super(`Session runtime queue ${dimension} capacity reached (current=${capacityDimensionCurrent(dimension, current)}, limit=${limit}).`);
32
+ this.dimension = dimension;
33
+ this.current = current;
34
+ this.limit = limit;
35
+ this.name = "RuntimeQueueCapacityError";
36
+ }
37
+ }
38
+ function capacityDimensionCurrent(dimension, current) {
39
+ if (dimension === "message_bytes")
40
+ return current.messageBytes;
41
+ if (dimension === "queue_count")
42
+ return current.queueCount;
43
+ if (dimension === "queue_bytes")
44
+ return current.queueBytes;
45
+ return current.oldestWaitMs;
46
+ }
47
+ function isRunReminderMessage(event) {
48
+ return event.source === "service" && event.text.startsWith("<pibo_run_notification>");
49
+ }
20
50
  function serializedToolArgs(value) {
21
51
  try {
22
52
  return JSON.stringify(value) ?? String(value);
@@ -146,6 +176,7 @@ export class RuntimeRoutedSession {
146
176
  modelFallbacks;
147
177
  suppressProviderFailures = false;
148
178
  pendingProviderFailure;
179
+ runReminderDeferredWarning;
149
180
  unsubscribe;
150
181
  constructor(piboSessionId, runtimeSession, emit, pluginRegistry, options = {}) {
151
182
  this.piboSessionId = piboSessionId;
@@ -166,8 +197,30 @@ export class RuntimeRoutedSession {
166
197
  if (this.sessionIdentityOperationInFlight && !this.forkCandidatesRequest) {
167
198
  throw new Error("Pibo session cannot accept messages while a session identity operation is in progress.");
168
199
  }
200
+ if (isRunReminderMessage(event))
201
+ this.removeQueuedMessages(isRunReminderMessage);
202
+ const now = this.options.now?.() ?? Date.now();
203
+ // Run reminders are coalesced service work. They remain bounded on admission,
204
+ // but cannot age-poison the ordinary message budget while an active turn runs.
205
+ const messages = this.queue.filter((item) => item.kind === "message" && !isRunReminderMessage(item.event));
206
+ const messageBytes = Buffer.byteLength(event.text);
207
+ const queueBytes = messages.reduce((total, item) => total + Buffer.byteLength(item.event.text), messageBytes);
208
+ const oldestWaitMs = messages.reduce((oldest, item) => Math.max(oldest, now - item.acceptedAt), 0);
209
+ const current = { messageBytes, queueCount: messages.length, queueBytes, oldestWaitMs };
210
+ if (messageBytes > RUNTIME_QUEUE_MAX_MESSAGE_BYTES) {
211
+ throw new RuntimeQueueCapacityError("message_bytes", current, RUNTIME_QUEUE_MAX_MESSAGE_BYTES);
212
+ }
213
+ if (messages.length >= RUNTIME_QUEUE_MAX_MESSAGES) {
214
+ throw new RuntimeQueueCapacityError("queue_count", current, RUNTIME_QUEUE_MAX_MESSAGES);
215
+ }
216
+ if (queueBytes > RUNTIME_QUEUE_MAX_BYTES) {
217
+ throw new RuntimeQueueCapacityError("queue_bytes", current, RUNTIME_QUEUE_MAX_BYTES);
218
+ }
219
+ if (oldestWaitMs >= RUNTIME_QUEUE_MAX_WAIT_MS) {
220
+ throw new RuntimeQueueCapacityError("oldest_wait_age", current, RUNTIME_QUEUE_MAX_WAIT_MS);
221
+ }
169
222
  onAccepted();
170
- this.queue.push({ kind: "message", event });
223
+ this.queue.push({ kind: "message", event, acceptedAt: now });
171
224
  const output = {
172
225
  type: "message_queued",
173
226
  piboSessionId: this.piboSessionId,
@@ -310,7 +363,9 @@ export class RuntimeRoutedSession {
310
363
  thinkingLevel,
311
364
  fastMode: status.fastMode?.mode === "fast",
312
365
  retry: status.retry,
313
- warnings: status.warnings,
366
+ warnings: this.runReminderDeferredWarning
367
+ ? [...new Set([...(status.warnings ?? []), this.runReminderDeferredWarning])]
368
+ : status.warnings,
314
369
  errors: status.errors,
315
370
  ...(pendingApprovals.length > 0
316
371
  ? { pendingApprovals: pendingApprovals.map((request) => structuredClone(request)) }
@@ -329,7 +384,9 @@ export class RuntimeRoutedSession {
329
384
  activeModel: status.activeModel,
330
385
  contextUsage: status.contextUsage,
331
386
  providerUsage: status.providerUsage,
332
- warnings: status.warnings,
387
+ warnings: this.runReminderDeferredWarning
388
+ ? [...new Set([...(status.warnings ?? []), this.runReminderDeferredWarning])]
389
+ : status.warnings,
333
390
  errors: status.errors,
334
391
  };
335
392
  }
@@ -376,8 +433,13 @@ export class RuntimeRoutedSession {
376
433
  }
377
434
  removedMessages.reverse();
378
435
  this.notifyMessagesInterrupted(removedMessages, "queued message removed");
436
+ if (removedMessages.length > 0)
437
+ this.notifyState();
379
438
  return removedMessages.length;
380
439
  }
440
+ setRunReminderDeferredWarning(message) {
441
+ this.runReminderDeferredWarning = message;
442
+ }
381
443
  getCurrentSession() {
382
444
  const getCurrentSession = this.runtimeSession.controls?.getCurrentSession;
383
445
  if (!getCurrentSession)
@@ -593,6 +655,7 @@ export class RuntimeRoutedSession {
593
655
  if (inFlight?.event.id === eventId) {
594
656
  if (!inFlight.cancellation) {
595
657
  inFlight.cancelled = true;
658
+ inFlight.capacityAbort.abort(new Error("Message cancelled while waiting for capacity."));
596
659
  this.notifyMessagesInterrupted([inFlight.event], "message cancelled");
597
660
  const active = this.activeMessage?.id === eventId;
598
661
  inFlight.cancellation = (async () => {
@@ -618,24 +681,23 @@ export class RuntimeRoutedSession {
618
681
  }
619
682
  return false;
620
683
  }
684
+ runtimeAssistantIndices = new Map();
621
685
  handleRuntimeEvent(event) {
622
686
  switch (event.type) {
623
687
  case "assistant_delta":
624
- this.emit(this.withActiveMessage({
625
- type: "assistant_delta",
688
+ case "assistant_message": {
689
+ const output = this.withActiveMessage({
690
+ type: event.type,
626
691
  piboSessionId: this.piboSessionId,
627
692
  text: event.text,
628
693
  contentIndex: event.contentIndex,
629
- }));
630
- return;
631
- case "assistant_message":
632
- this.emit(this.withActiveMessage({
633
- type: "assistant_message",
634
- piboSessionId: this.piboSessionId,
635
- text: event.text,
636
- contentIndex: event.contentIndex,
637
- }));
694
+ });
695
+ if ((output.type === "assistant_delta" || output.type === "assistant_message") && event.contentIndex !== undefined && output.assistantIndex !== undefined) {
696
+ this.runtimeAssistantIndices.set(event.contentIndex, output.assistantIndex);
697
+ }
698
+ this.emit(output);
638
699
  return;
700
+ }
639
701
  case "reasoning_started":
640
702
  this.emit(this.withActiveMessage({
641
703
  type: "thinking_started",
@@ -677,6 +739,12 @@ export class RuntimeRoutedSession {
677
739
  this.emit(this.withActiveMessage({
678
740
  type: "assistant_usage",
679
741
  piboSessionId: this.piboSessionId,
742
+ ...(event.inferenceId ? { inferenceId: event.inferenceId } : {}),
743
+ ...(event.target ? { inferenceTarget: event.target.type === "assistant"
744
+ ? (this.runtimeAssistantIndices.has(event.target.contentIndex)
745
+ ? { type: "assistant", assistantIndex: this.runtimeAssistantIndices.get(event.target.contentIndex) }
746
+ : { type: "turn" })
747
+ : event.target } : {}),
680
748
  inputTokens: event.usage.inputTokens,
681
749
  outputTokens: event.usage.outputTokens,
682
750
  cacheReadTokens: event.usage.cacheReadTokens,
@@ -710,6 +778,7 @@ export class RuntimeRoutedSession {
710
778
  result: event.result,
711
779
  aborted: event.aborted,
712
780
  errorMessage: event.errorMessage,
781
+ compactionStats: event.compactionStats,
713
782
  }));
714
783
  return;
715
784
  case "approval_requested":
@@ -815,11 +884,29 @@ export class RuntimeRoutedSession {
815
884
  this.notifyState();
816
885
  }
817
886
  }
887
+ async promptWithinCapacity(event, inFlight, input) {
888
+ const provider = this.runtimeSession.getStatus().activeModel?.provider ?? this.runtimeSession.runtimeInstanceId;
889
+ const lease = await this.options.acquireProviderCapacity?.(provider, inFlight.capacityAbort.signal);
890
+ try {
891
+ if (this.disposed || inFlight.cancelled)
892
+ return;
893
+ if (!inFlight.started) {
894
+ inFlight.started = true;
895
+ this.emit({ type: "message_started", piboSessionId: this.piboSessionId, eventId: event.id, text: event.text, source: event.source, provenance: event.provenance });
896
+ }
897
+ if (this.disposed || inFlight.cancelled)
898
+ return;
899
+ await this.runtimeSession.prompt(input);
900
+ }
901
+ finally {
902
+ lease?.release();
903
+ }
904
+ }
818
905
  async promptWithModelFallbacks(event, inFlight) {
819
906
  const canSetModel = Boolean(this.runtimeSession.controls?.setModel);
820
907
  const fallbackModels = this.modelFallbacks.filter((model) => !sameModel(model, this.primaryModel));
821
908
  if (!canSetModel || fallbackModels.length === 0) {
822
- await this.runtimeSession.prompt({
909
+ await this.promptWithinCapacity(event, inFlight, {
823
910
  text: event.text,
824
911
  source: promptSource(event.source),
825
912
  capabilityScope: event.capabilityScope,
@@ -845,7 +932,7 @@ export class RuntimeRoutedSession {
845
932
  this.pendingProviderFailure = undefined;
846
933
  let promptError;
847
934
  try {
848
- await this.runtimeSession.prompt({
935
+ await this.promptWithinCapacity(event, inFlight, {
849
936
  text: attempt === 0 ? event.text : PIBO_PROVIDER_RECOVERY_PROMPT,
850
937
  source: attempt === 0 ? promptSource(event.source) : "rpc",
851
938
  capabilityScope: event.capabilityScope,
@@ -907,14 +994,6 @@ export class RuntimeRoutedSession {
907
994
  this.activeMessageFailed = false;
908
995
  this.beginRunReminderTurnGuard(event);
909
996
  this.resetContentIndices();
910
- this.emit({
911
- type: "message_started",
912
- piboSessionId: this.piboSessionId,
913
- eventId: event.id,
914
- text: event.text,
915
- source: event.source,
916
- provenance: event.provenance,
917
- });
918
997
  if (inFlight.cancelled)
919
998
  return;
920
999
  await this.promptWithModelFallbacks(event, inFlight);
@@ -963,6 +1042,7 @@ export class RuntimeRoutedSession {
963
1042
  const inFlight = {
964
1043
  event,
965
1044
  cancelled: false,
1045
+ capacityAbort: new AbortController(),
966
1046
  settled: new Promise((resolve) => { resolveSettled = resolve; }),
967
1047
  resolveSettled: () => { resolveSettled?.(); },
968
1048
  };
@@ -1129,8 +1209,12 @@ export class RuntimeRoutedSession {
1129
1209
  ? await this.options.logoutRuntimeAuth(input)
1130
1210
  : await this.pluginRegistry.logoutAgentRuntimeAuth(this.runtimeSession.runtimeInstanceId, input),
1131
1211
  getProviderUsage: () => this.getActionProviderUsage(),
1132
- clearQueue: () => this.clearQueue(),
1212
+ clearQueue: () => this.clearQueue() + previouslyClearedMessages(event),
1133
1213
  abort: async () => {
1214
+ if (this.inFlightMessage && !this.inFlightMessage.started) {
1215
+ await this.cancelMessage(this.inFlightMessage.event.id);
1216
+ return;
1217
+ }
1134
1218
  if (this.activeMessage)
1135
1219
  this.notifyMessagesInterrupted([this.activeMessage], "abort requested");
1136
1220
  await this.runtimeSession.abort();
@@ -1187,6 +1271,7 @@ export class RuntimeRoutedSession {
1187
1271
  if (this.disposed)
1188
1272
  return false;
1189
1273
  const activeMessage = this.activeMessage;
1274
+ this.inFlightMessage?.capacityAbort.abort(new Error(reason));
1190
1275
  this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), reason);
1191
1276
  if (activeMessage) {
1192
1277
  const error = `Session disposed while a message was active: ${reason}`;
@@ -1262,6 +1347,7 @@ export class RuntimeRoutedSession {
1262
1347
  }
1263
1348
  resetContentIndices() {
1264
1349
  this.closeActiveContentParts();
1350
+ this.runtimeAssistantIndices.clear();
1265
1351
  this.nextAssistantIndex = 0;
1266
1352
  this.nextThinkingIndex = 0;
1267
1353
  }
@@ -245,6 +245,9 @@ function newPendingTurn(kind = "turn") {
245
245
  compactionEnded: false,
246
246
  startedTools: new Map(),
247
247
  completedTools: new Set(),
248
+ usageSnapshots: new Set(),
249
+ usageItemOffset: 0,
250
+ nextUsageIndex: 0,
248
251
  };
249
252
  }
250
253
  export class CodexNativeTurnController {
@@ -611,7 +614,30 @@ export class CodexNativeTurnController {
611
614
  const params = this.scopedTurnParams(value, "thread token usage");
612
615
  if (!params)
613
616
  return;
614
- params.pending.usage = validateTokenUsage(params.record.tokenUsage);
617
+ const snapshot = requiredRecord(params.record.tokenUsage, "thread token usage");
618
+ const usage = validateTokenUsage(snapshot);
619
+ const total = tokenUsageBreakdown(snapshot.total, "cumulative thread usage");
620
+ const key = JSON.stringify(total);
621
+ const pending = params.pending;
622
+ // Codex repeats snapshots for quota/replay notifications. Equal last usage
623
+ // alone is not a duplicate: two real requests may use identical token counts.
624
+ if (pending.usageSnapshots.has(key))
625
+ return;
626
+ pending.usageSnapshots.add(key);
627
+ const newItems = pending.itemOrder.slice(pending.usageItemOffset);
628
+ pending.usageItemOffset = pending.itemOrder.length;
629
+ let target = { type: "turn" };
630
+ // A usage update closes the observed output span. Only newly introduced
631
+ // items belong to it; late tool completions cannot steal another step's usage.
632
+ // Several tools in one response share one record, anchored at its last output.
633
+ for (const id of newItems) {
634
+ const item = pending.items.get(id);
635
+ if (TOOL_ITEM_TYPES.has(item.type))
636
+ target = { type: "tool", toolCallId: id };
637
+ else if (item.type === "agentMessage")
638
+ target = { type: "assistant", contentIndex: this.assistantIndex(pending, id) };
639
+ }
640
+ this.emit({ type: "usage", usage, inferenceId: `${pending.turnId}:usage:${pending.nextUsageIndex++}`, target });
615
641
  }
616
642
  handleThreadStatus(value) {
617
643
  const params = requiredRecord(value, "thread status notification");
@@ -845,8 +871,6 @@ export class CodexNativeTurnController {
845
871
  for (const item of turn.items)
846
872
  this.completeItem(pending, item);
847
873
  this.finishIncompleteItems(pending, turn.status);
848
- if (pending.usage)
849
- this.emit({ type: "usage", usage: pending.usage });
850
874
  const persistedItems = pending.itemOrder
851
875
  .map((itemId) => pending.items.get(itemId))
852
876
  .filter((item) => item !== undefined);
@@ -253,6 +253,7 @@ export function semanticEventFromPibo(event) {
253
253
  result: event.result,
254
254
  aborted: event.aborted,
255
255
  errorMessage: event.errorMessage,
256
+ compactionStats: event.compactionStats,
256
257
  };
257
258
  case "session_error":
258
259
  return { type: "error", message: event.error, details: event.errorDetails };
@@ -863,8 +863,6 @@ export class RoutedSession {
863
863
  if (assistantMessageEnded && !assistantMessageFailed)
864
864
  this.flushPendingProviderWebSearchFinishes();
865
865
  const usageEvent = assistantMessageEnded ? normalizeAssistantUsageEvent(this.piboSessionId, candidate?.message) : undefined;
866
- if (usageEvent)
867
- this.emit(this.withActiveMessage(usageEvent));
868
866
  // Pi gets the first chance to recover through its short retry/compaction loop.
869
867
  // Keep the final error pending so the routed turn can continue durable recovery.
870
868
  if (assistantMessageEnded && normalized?.type === "session_error") {
@@ -889,6 +887,8 @@ export class RoutedSession {
889
887
  this.emit(this.withActiveMessage(normalized));
890
888
  }
891
889
  }
890
+ if (usageEvent)
891
+ this.emit(this.withActiveMessage(usageEvent));
892
892
  if (this.forwardPiEvents) {
893
893
  this.emit({ type: "pi_event", piboSessionId: this.piboSessionId, event });
894
894
  }
@@ -0,0 +1,98 @@
1
+ /** Owns the only application queue; HWM zero prevents a second hidden stream queue. */
2
+ export class BoundedEventStream {
3
+ onClose;
4
+ maximumBytes;
5
+ maximumAgeMs;
6
+ maximumFrames;
7
+ stream;
8
+ writer;
9
+ controller;
10
+ queue = [];
11
+ bytes = 0;
12
+ stopped = false;
13
+ finishing = false;
14
+ pendingPull;
15
+ capacityWaiter;
16
+ timer;
17
+ constructor(onClose, maximumBytes = 2 * 1024 * 1024, maximumAgeMs = 5000, maximumFrames = 4096) {
18
+ this.onClose = onClose;
19
+ this.maximumBytes = maximumBytes;
20
+ this.maximumAgeMs = maximumAgeMs;
21
+ this.maximumFrames = maximumFrames;
22
+ this.stream = new ReadableStream({
23
+ start: controller => { this.controller = controller; },
24
+ pull: () => {
25
+ const frame = this.queue.shift();
26
+ if (frame) {
27
+ this.bytes -= frame.value.byteLength;
28
+ this.controller.enqueue(frame.value);
29
+ this.releaseCapacity();
30
+ if (this.finishing && !this.queue.length)
31
+ this.stop("complete");
32
+ return;
33
+ }
34
+ if (this.finishing || this.stopped) {
35
+ this.stop("complete");
36
+ return;
37
+ }
38
+ return new Promise(resolve => { this.pendingPull = resolve; });
39
+ },
40
+ cancel: () => this.stop("cancel"),
41
+ }, { highWaterMark: 0 });
42
+ const self = this;
43
+ this.writer = { enqueue: value => { this.enqueue(value); }, close: () => this.finish(), error: () => this.stop("error"), get desiredSize() { return self.stopped ? null : self.maximumBytes - self.bytes; } };
44
+ this.timer = setInterval(() => { if (this.queue.length && performance.now() - this.queue[0].at > this.maximumAgeMs)
45
+ this.stop("slow_age"); }, Math.min(1000, Math.max(5, maximumAgeMs / 2)));
46
+ this.timer.unref();
47
+ }
48
+ get closed() { return this.stopped || this.finishing; }
49
+ enqueue(value) {
50
+ if (this.closed)
51
+ return;
52
+ if (value.byteLength > this.maximumBytes || this.bytes + value.byteLength > this.maximumBytes || this.queue.length >= this.maximumFrames) {
53
+ this.stop("slow_bytes");
54
+ return;
55
+ }
56
+ if (this.pendingPull) {
57
+ const resolve = this.pendingPull;
58
+ this.pendingPull = undefined;
59
+ this.controller.enqueue(value);
60
+ resolve();
61
+ return;
62
+ }
63
+ this.queue.push({ value, at: performance.now() });
64
+ this.bytes += value.byteLength;
65
+ }
66
+ async waitForCapacity() { if (this.closed)
67
+ return false; if (this.bytes < this.maximumBytes / 2)
68
+ return true; return new Promise(resolve => { this.capacityWaiter = resolve; }); }
69
+ releaseCapacity() { if (this.bytes < this.maximumBytes / 2) {
70
+ this.capacityWaiter?.(!this.closed);
71
+ this.capacityWaiter = undefined;
72
+ } }
73
+ finish() { if (this.stopped)
74
+ return; this.finishing = true; if (!this.queue.length)
75
+ this.stop("complete"); }
76
+ fail() { this.stop("error"); }
77
+ stop(reason) {
78
+ if (this.stopped)
79
+ return;
80
+ this.stopped = true;
81
+ clearInterval(this.timer);
82
+ this.queue.length = 0;
83
+ this.bytes = 0;
84
+ try {
85
+ if (reason === "complete")
86
+ this.controller.close();
87
+ else if (reason !== "cancel")
88
+ this.controller.error(Error("Event stream budget exhausted; reconnect using the last received cursor."));
89
+ }
90
+ catch { }
91
+ this.pendingPull?.();
92
+ this.pendingPull = undefined;
93
+ this.capacityWaiter?.(false);
94
+ this.capacityWaiter = undefined;
95
+ this.onClose(reason);
96
+ }
97
+ status() { return { frames: this.queue.length, bytes: this.bytes, oldestAgeMs: this.queue.length ? performance.now() - this.queue[0].at : 0, closed: this.closed }; }
98
+ }
@@ -8,7 +8,7 @@ import { parseToolMetricTokenCalculation } from "../../shared/tool-call-token-se
8
8
  import { PiboWebHttpError, readJsonBody, responseJson } from "../../web/http.js";
9
9
  import { CHAT_WEB_API_PREFIX } from "./chat-api-routes.js";
10
10
  import { normalizeBasePromptMarkdown, normalizeBasePromptMode, normalizeCompactionPromptMarkdown, normalizeCompactionPromptMode, updateChatModelDefaults, } from "./chat-request-normalizers.js";
11
- import { pruneTelemetryOlderThan } from "./telemetry-retention-service.js";
11
+ import { pruneTelemetryOlderThanAsync } from "./telemetry-retention-service.js";
12
12
  export function chatSettingsRoute(pathname, method) {
13
13
  if (pathname === `${CHAT_WEB_API_PREFIX}/model-defaults` && method === "PATCH")
14
14
  return { kind: "model-defaults" };
@@ -70,8 +70,8 @@ export async function handleChatSettingsRoute(input) {
70
70
  const days = sanitizeTelemetryRetentionDays(body.days);
71
71
  if (!days)
72
72
  throw new PiboWebHttpError("Invalid telemetry retention days", 400);
73
- const result = pruneTelemetryOlderThan({ dataStore: input.dataStore, days, apply: body.dryRun !== true });
74
- if (result.applied)
73
+ const result = await pruneTelemetryOlderThanAsync({ dataStore: input.dataStore, days, apply: body.dryRun !== true, signal: request.signal });
74
+ if (result.applied && result.completed !== false)
75
75
  updateTelemetryRetentionLastPrunedAt(new Date().toISOString());
76
76
  return responseJson({ telemetryRetention: result });
77
77
  }
@@ -3,13 +3,31 @@ import { roomWorkspaceFromMetadata } from "../types/rooms.js";
3
3
  import { tracePayloadRefForStoredPayload } from "../trace-v2.js";
4
4
  import { qualifiedToolNodeId } from "../../../shared/trace-tool-identity.js";
5
5
  const MAX_TRACE_EVENT_HYDRATION_BYTES = 1024 * 1024;
6
- export function storedPiboEventFromV2Row(row, payloadStore) {
6
+ export function storedPiboEventFromV2Row(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
7
+ if (typeof hydrationBytes !== "number" || !Number.isFinite(hydrationBytes))
8
+ hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES;
7
9
  const attributes = parseJsonObject(row.attributes_json);
8
- const payload = outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore));
10
+ if (hydrationBytes < MAX_TRACE_EVENT_HYDRATION_BYTES && typeof attributes.inlineText === "string" && Buffer.byteLength(attributes.inlineText) > hydrationBytes)
11
+ attributes.inlineText = row.preview_text ?? "";
12
+ const payload = outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore, hydrationBytes));
9
13
  if (!payload)
10
14
  return undefined;
15
+ const storedPayloadRef = storedPayloadReference(row, attributes, payloadStore);
16
+ const renderSequence = numberAttribute(attributes, "renderSequence") ?? payload.renderSequence;
17
+ return { id: String(row.stream_id), piboSessionId: row.session_id ?? undefined, eventSequence: row.session_sequence ?? undefined, renderSequence, eventId: row.event_id ?? undefined, streamId: row.stream_id, storedPayloadRef, type: row.type, createdAt: row.created_at, payload };
18
+ }
19
+ export function storedChatEventFromV2Row(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
20
+ if (typeof hydrationBytes !== "number" || !Number.isFinite(hydrationBytes))
21
+ hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES;
22
+ const attributes = parseJsonObject(row.attributes_json);
23
+ if (hydrationBytes < MAX_TRACE_EVENT_HYDRATION_BYTES && typeof attributes.inlineText === "string" && Buffer.byteLength(attributes.inlineText) > hydrationBytes)
24
+ attributes.inlineText = row.preview_text ?? "";
25
+ const storedPayloadRef = storedPayloadReference(row, attributes, payloadStore);
26
+ return { storedPayloadRef, streamId: row.stream_id, roomId: row.room_id ?? undefined, piboSessionId: row.session_id ?? undefined, eventId: row.event_id ?? `evt_${row.stream_id}`, eventType: row.type, actorType: actorTypeValue(row.actor_type), actorId: row.actor_id ?? undefined, clientTxnId: typeof attributes.clientTxnId === "string" ? attributes.clientTxnId : undefined, createdAt: row.created_at, retentionClass: retentionClassValue(row.retention_class), payload: (outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore, hydrationBytes)) ?? null) };
27
+ }
28
+ function storedPayloadReference(row, attributes, payloadStore) {
11
29
  const payloadIdentity = tracePayloadIdentityForEvent(row, attributes);
12
- const storedPayloadRef = row.payload_ref && row.session_id && payloadStore && payloadIdentity
30
+ return row.payload_ref && row.session_id && payloadStore && payloadIdentity
13
31
  ? tracePayloadRefForStoredPayload({
14
32
  payloadStore,
15
33
  piboSessionId: row.session_id,
@@ -17,16 +35,15 @@ export function storedPiboEventFromV2Row(row, payloadStore) {
17
35
  ...payloadIdentity,
18
36
  })
19
37
  : undefined;
20
- const renderSequence = numberAttribute(attributes, "renderSequence") ?? payload.renderSequence;
21
- return { id: String(row.stream_id), piboSessionId: row.session_id ?? undefined, eventSequence: row.session_sequence ?? undefined, renderSequence, eventId: row.event_id ?? undefined, streamId: row.stream_id, storedPayloadRef, type: row.type, createdAt: row.created_at, payload };
22
- }
23
- export function storedChatEventFromV2Row(row, payloadStore) {
24
- const attributes = parseJsonObject(row.attributes_json);
25
- return { streamId: row.stream_id, roomId: row.room_id ?? undefined, piboSessionId: row.session_id ?? undefined, eventId: row.event_id ?? `evt_${row.stream_id}`, eventType: row.type, actorType: actorTypeValue(row.actor_type), actorId: row.actor_id ?? undefined, clientTxnId: typeof attributes.clientTxnId === "string" ? attributes.clientTxnId : undefined, createdAt: row.created_at, retentionClass: retentionClassValue(row.retention_class), payload: (outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore)) ?? null) };
26
38
  }
27
39
  function outputPayloadFromV2Row(row, attributes, persistedPayload) {
28
40
  const inlinePayload = attributes.inlinePayload ?? persistedPayload;
29
- if (inlinePayload && typeof inlinePayload === "object" && !Array.isArray(inlinePayload) && typeof inlinePayload.type === "string") {
41
+ if (inlinePayload
42
+ && typeof inlinePayload === "object"
43
+ && !Array.isArray(inlinePayload)
44
+ && inlinePayload.type === row.type
45
+ && typeof inlinePayload.piboSessionId === "string"
46
+ && inlinePayload.piboSessionId === row.session_id) {
30
47
  return compactObject({
31
48
  ...inlinePayload,
32
49
  renderSequence: numberAttribute(attributes, "renderSequence") ?? inlinePayload.renderSequence,
@@ -73,6 +90,8 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
73
90
  ...base,
74
91
  type: "assistant_usage",
75
92
  usageIndex: numberAttribute(attributes, "usageIndex"),
93
+ inferenceId: stringAttribute(attributes, "inferenceId"),
94
+ inferenceTarget: attributes.inferenceTarget,
76
95
  inputTokens: numberAttribute(attributes, "inputTokens"),
77
96
  outputTokens: numberAttribute(attributes, "outputTokens"),
78
97
  cacheReadTokens: numberAttribute(attributes, "cacheReadTokens"),
@@ -112,7 +131,7 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
112
131
  if (row.type === "compaction_start")
113
132
  return { ...base, type: "compaction_start", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown" };
114
133
  if (row.type === "compaction_end")
115
- return { ...base, type: "compaction_end", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown", result: inlinePayload, aborted: booleanAttribute(attributes, "aborted") ?? false, errorMessage: stringAttribute(attributes, "errorMessage") };
134
+ return { ...base, type: "compaction_end", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown", result: inlinePayload, aborted: booleanAttribute(attributes, "aborted") ?? false, errorMessage: stringAttribute(attributes, "errorMessage"), compactionStats: isRecord(attributes.compactionStats) ? attributes.compactionStats : undefined };
116
135
  if (row.type === "session_error") {
117
136
  const error = stringAttribute(attributes, "error") ?? row.preview_text ?? "Error";
118
137
  return { ...base, type: "session_error", error, errorDetails: normalizeSessionErrorDetails(error, isRecord(attributes.errorDetails) ? attributes.errorDetails : undefined) };
@@ -121,14 +140,14 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
121
140
  return { type: "user.message.accepted", piboSessionId, roomId: row.room_id ?? undefined, text: stringAttribute(attributes, "inlineText") ?? row.preview_text ?? "", clientTxnId: stringAttribute(attributes, "clientTxnId") };
122
141
  return { ...base, type: row.type };
123
142
  }
124
- function readPersistedPayload(row, payloadStore) {
143
+ function readPersistedPayload(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
125
144
  if (!row.payload_ref || !payloadStore)
126
145
  return undefined;
127
146
  try {
128
147
  const metadata = payloadStore.getPayload(row.payload_ref);
129
- if (!metadata || metadata.byteSize > MAX_TRACE_EVENT_HYDRATION_BYTES)
148
+ if (!metadata || metadata.byteSize > hydrationBytes)
130
149
  return undefined;
131
- const text = Buffer.from(payloadStore.readPayloadBytesBounded(row.payload_ref, MAX_TRACE_EVENT_HYDRATION_BYTES)).toString("utf8");
150
+ const text = Buffer.from(payloadStore.readPayloadBytesBounded(row.payload_ref, hydrationBytes)).toString("utf8");
132
151
  return metadata.contentType.includes("json") ? JSON.parse(text) : text;
133
152
  }
134
153
  catch {
@@ -136,6 +155,13 @@ function readPersistedPayload(row, payloadStore) {
136
155
  }
137
156
  }
138
157
  function tracePayloadIdentityForEvent(row, attributes) {
158
+ const semanticId = stringAttribute(attributes, "semanticEventId") ?? row.event_id;
159
+ if (semanticId && (row.type === "assistant_message" || row.type === "thinking_finished")) {
160
+ const thinking = row.type === "thinking_finished";
161
+ const index = numberAttribute(attributes, thinking ? "thinkingIndex" : "assistantIndex") ?? numberAttribute(attributes, "contentIndex");
162
+ const identity = index === undefined ? semanticId : `${semanticId}:${thinking ? "thinking" : "assistant"}:${index}`;
163
+ return { nodeId: `${thinking ? "reasoning" : "assistant"}:${identity}`, payloadKind: thinking ? "reasoning" : "output" };
164
+ }
139
165
  const toolCallId = stringAttribute(attributes, "toolCallId");
140
166
  if (!toolCallId)
141
167
  return undefined;
@@ -1,6 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { isLiveOnlyOutputEvent } from "../output-event-policy.js";
3
3
  import { compactObject, nextSessionSequence, previewForPayload, storedChatEventFromV2Row } from "./chat-data-mappers.js";
4
+ export function chatClientTransactionKey(roomId, actorId, clientTxnId) {
5
+ return `chat:user.accepted:${roomId ?? ""}:${actorId ?? ""}:${clientTxnId}`;
6
+ }
4
7
  export class ChatEventCommandService {
5
8
  store;
6
9
  constructor(store) {
@@ -9,10 +12,7 @@ export class ChatEventCommandService {
9
12
  appendEvent(input) {
10
13
  const createdAt = input.createdAt ?? new Date().toISOString();
11
14
  const eventId = input.eventId ?? `ce_${randomUUID()}`;
12
- const idempotencyKey = input.clientTxnId ? `chat:user.accepted:${input.roomId ?? ""}:${input.actorId ?? ""}:${input.clientTxnId}` : `chat:event:${eventId}`;
13
- const existing = input.clientTxnId ? this.findByClientTxn(input.roomId, input.actorId, input.clientTxnId) : undefined;
14
- if (existing)
15
- return existing;
15
+ const idempotencyKey = input.clientTxnId ? chatClientTransactionKey(input.roomId, input.actorId, input.clientTxnId) : `chat:event:${eventId}`;
16
16
  const stored = this.store.eventLog.appendEvent({
17
17
  sessionId: input.piboSessionId,
18
18
  sessionSequence: input.piboSessionId ? nextSessionSequence(this.store, input.piboSessionId) : undefined,
@@ -30,21 +30,7 @@ export class ChatEventCommandService {
30
30
  createdAt,
31
31
  indexedAt: createdAt,
32
32
  });
33
- return storedChatEventFromV2Row({
34
- stream_id: stored.streamId,
35
- session_id: stored.sessionId ?? null,
36
- session_sequence: stored.sessionSequence ?? null,
37
- room_id: stored.roomId ?? null,
38
- type: stored.type,
39
- actor_type: stored.actorType ?? null,
40
- actor_id: stored.actorId ?? null,
41
- event_id: stored.eventId ?? null,
42
- idempotency_key: stored.idempotencyKey ?? null,
43
- retention_class: stored.retentionClass,
44
- preview_text: stored.previewText ?? null,
45
- attributes_json: JSON.stringify(stored.attributes),
46
- created_at: stored.createdAt,
47
- });
33
+ return chatEventFromStoredRow(stored);
48
34
  }
49
35
  appendOutputEvent(event, _input = {}) {
50
36
  // Runtime output ingestion is owned by ChatDataIngestService. Avoid a second event_log insert here.
@@ -55,8 +41,14 @@ export class ChatEventCommandService {
55
41
  findByClientTxn(roomId, actorId, clientTxnId) {
56
42
  if (!roomId || !actorId)
57
43
  return undefined;
58
- const row = this.store.db.prepare("SELECT * FROM event_log WHERE room_id = ? AND actor_id = ? AND json_extract(attributes_json, '$.clientTxnId') = ? ORDER BY stream_id ASC LIMIT 1").get(roomId, actorId, clientTxnId);
59
- return row ? storedChatEventFromV2Row(row) : undefined;
44
+ // Empty IDs are not valid HTTP transaction keys. Retain the legacy direct-call
45
+ // lookup contract without adding a scan fallback to ordinary indexed misses.
46
+ if (clientTxnId === "") {
47
+ const row = this.store.db.prepare("SELECT * FROM event_log WHERE room_id = ? AND actor_id = ? AND json_extract(attributes_json, '$.clientTxnId') = ? ORDER BY stream_id ASC LIMIT 1").get(roomId, actorId, clientTxnId);
48
+ return row ? storedChatEventFromV2Row(row) : undefined;
49
+ }
50
+ const row = this.store.eventLog.findByIdempotencyKey(chatClientTransactionKey(roomId, actorId, clientTxnId));
51
+ return row ? chatEventFromStoredRow(row) : undefined;
60
52
  }
61
53
  deleteSessions(piboSessionIds) {
62
54
  if (!piboSessionIds.length)
@@ -73,3 +65,20 @@ export class ChatEventCommandService {
73
65
  return Number(result.changes ?? 0);
74
66
  }
75
67
  }
68
+ function chatEventFromStoredRow(stored) {
69
+ return storedChatEventFromV2Row({
70
+ stream_id: stored.streamId,
71
+ session_id: stored.sessionId ?? null,
72
+ session_sequence: stored.sessionSequence ?? null,
73
+ room_id: stored.roomId ?? null,
74
+ type: stored.type,
75
+ actor_type: stored.actorType ?? null,
76
+ actor_id: stored.actorId ?? null,
77
+ event_id: stored.eventId ?? null,
78
+ idempotency_key: stored.idempotencyKey ?? null,
79
+ retention_class: stored.retentionClass,
80
+ preview_text: stored.previewText ?? null,
81
+ attributes_json: JSON.stringify(stored.attributes),
82
+ created_at: stored.createdAt,
83
+ });
84
+ }