@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
@@ -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";
@@ -166,8 +167,14 @@ export class RuntimeRoutedSession {
166
167
  if (this.sessionIdentityOperationInFlight && !this.forkCandidatesRequest) {
167
168
  throw new Error("Pibo session cannot accept messages while a session identity operation is in progress.");
168
169
  }
170
+ const messages = this.queue.filter(item => item.kind === "message");
171
+ const bytes = Buffer.byteLength(event.text);
172
+ if (bytes > 1024 * 1024 || messages.length >= 64 || messages.reduce((total, item) => total + Buffer.byteLength(item.event.text), bytes) > 4 * 1024 * 1024
173
+ || messages.some(item => Date.now() - item.acceptedAt >= 10 * 60 * 1000)) {
174
+ throw Object.assign(new Error("Session runtime queue count, byte or wait-age capacity reached."), { code: "runtime_capacity_unavailable" });
175
+ }
169
176
  onAccepted();
170
- this.queue.push({ kind: "message", event });
177
+ this.queue.push({ kind: "message", event, acceptedAt: Date.now() });
171
178
  const output = {
172
179
  type: "message_queued",
173
180
  piboSessionId: this.piboSessionId,
@@ -297,6 +304,8 @@ export class RuntimeRoutedSession {
297
304
  revision: binding.revision,
298
305
  },
299
306
  queuedMessages: this.queue.length,
307
+ activeEventId: this.activeMessage?.id ?? this.activeExecutionEvent?.id,
308
+ queuedEventIds: this.queue.map((item) => item.event.id ?? ""),
300
309
  processing: this.disposed ? false : this.processing,
301
310
  streaming: this.disposed ? false : status.streaming,
302
311
  activeTools: [...status.enabledTools],
@@ -591,6 +600,7 @@ export class RuntimeRoutedSession {
591
600
  if (inFlight?.event.id === eventId) {
592
601
  if (!inFlight.cancellation) {
593
602
  inFlight.cancelled = true;
603
+ inFlight.capacityAbort.abort(new Error("Message cancelled while waiting for capacity."));
594
604
  this.notifyMessagesInterrupted([inFlight.event], "message cancelled");
595
605
  const active = this.activeMessage?.id === eventId;
596
606
  inFlight.cancellation = (async () => {
@@ -708,6 +718,7 @@ export class RuntimeRoutedSession {
708
718
  result: event.result,
709
719
  aborted: event.aborted,
710
720
  errorMessage: event.errorMessage,
721
+ compactionStats: event.compactionStats,
711
722
  }));
712
723
  return;
713
724
  case "approval_requested":
@@ -813,11 +824,29 @@ export class RuntimeRoutedSession {
813
824
  this.notifyState();
814
825
  }
815
826
  }
827
+ async promptWithinCapacity(event, inFlight, input) {
828
+ const provider = this.runtimeSession.getStatus().activeModel?.provider ?? this.runtimeSession.runtimeInstanceId;
829
+ const lease = await this.options.acquireProviderCapacity?.(provider, inFlight.capacityAbort.signal);
830
+ try {
831
+ if (this.disposed || inFlight.cancelled)
832
+ return;
833
+ if (!inFlight.started) {
834
+ inFlight.started = true;
835
+ this.emit({ type: "message_started", piboSessionId: this.piboSessionId, eventId: event.id, text: event.text, source: event.source, provenance: event.provenance });
836
+ }
837
+ if (this.disposed || inFlight.cancelled)
838
+ return;
839
+ await this.runtimeSession.prompt(input);
840
+ }
841
+ finally {
842
+ lease?.release();
843
+ }
844
+ }
816
845
  async promptWithModelFallbacks(event, inFlight) {
817
846
  const canSetModel = Boolean(this.runtimeSession.controls?.setModel);
818
847
  const fallbackModels = this.modelFallbacks.filter((model) => !sameModel(model, this.primaryModel));
819
848
  if (!canSetModel || fallbackModels.length === 0) {
820
- await this.runtimeSession.prompt({
849
+ await this.promptWithinCapacity(event, inFlight, {
821
850
  text: event.text,
822
851
  source: promptSource(event.source),
823
852
  capabilityScope: event.capabilityScope,
@@ -843,7 +872,7 @@ export class RuntimeRoutedSession {
843
872
  this.pendingProviderFailure = undefined;
844
873
  let promptError;
845
874
  try {
846
- await this.runtimeSession.prompt({
875
+ await this.promptWithinCapacity(event, inFlight, {
847
876
  text: attempt === 0 ? event.text : PIBO_PROVIDER_RECOVERY_PROMPT,
848
877
  source: attempt === 0 ? promptSource(event.source) : "rpc",
849
878
  capabilityScope: event.capabilityScope,
@@ -905,14 +934,6 @@ export class RuntimeRoutedSession {
905
934
  this.activeMessageFailed = false;
906
935
  this.beginRunReminderTurnGuard(event);
907
936
  this.resetContentIndices();
908
- this.emit({
909
- type: "message_started",
910
- piboSessionId: this.piboSessionId,
911
- eventId: event.id,
912
- text: event.text,
913
- source: event.source,
914
- provenance: event.provenance,
915
- });
916
937
  if (inFlight.cancelled)
917
938
  return;
918
939
  await this.promptWithModelFallbacks(event, inFlight);
@@ -961,6 +982,7 @@ export class RuntimeRoutedSession {
961
982
  const inFlight = {
962
983
  event,
963
984
  cancelled: false,
985
+ capacityAbort: new AbortController(),
964
986
  settled: new Promise((resolve) => { resolveSettled = resolve; }),
965
987
  resolveSettled: () => { resolveSettled?.(); },
966
988
  };
@@ -1127,8 +1149,12 @@ export class RuntimeRoutedSession {
1127
1149
  ? await this.options.logoutRuntimeAuth(input)
1128
1150
  : await this.pluginRegistry.logoutAgentRuntimeAuth(this.runtimeSession.runtimeInstanceId, input),
1129
1151
  getProviderUsage: () => this.getActionProviderUsage(),
1130
- clearQueue: () => this.clearQueue(),
1152
+ clearQueue: () => this.clearQueue() + previouslyClearedMessages(event),
1131
1153
  abort: async () => {
1154
+ if (this.inFlightMessage && !this.inFlightMessage.started) {
1155
+ await this.cancelMessage(this.inFlightMessage.event.id);
1156
+ return;
1157
+ }
1132
1158
  if (this.activeMessage)
1133
1159
  this.notifyMessagesInterrupted([this.activeMessage], "abort requested");
1134
1160
  await this.runtimeSession.abort();
@@ -1185,6 +1211,7 @@ export class RuntimeRoutedSession {
1185
1211
  if (this.disposed)
1186
1212
  return false;
1187
1213
  const activeMessage = this.activeMessage;
1214
+ this.inFlightMessage?.capacityAbort.abort(new Error(reason));
1188
1215
  this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), reason);
1189
1216
  if (activeMessage) {
1190
1217
  const error = `Session disposed while a message was active: ${reason}`;
@@ -16,6 +16,7 @@ import { CodexNativeResourceDelivery } from "./resource-delivery.js";
16
16
  import { CODEX_NATIVE_MODEL_OPTIONS_SCHEMA, CODEX_NATIVE_MODEL_PROVIDER_ID, CODEX_NATIVE_REASONING_VALUES, CodexNativeSessionSettingsController, parseCodexNativeProfileOptions, readCodexNativeModelCatalog, readCodexNativePersistedSettings, toAgentRuntimeModelCatalog, } from "./models.js";
17
17
  import { CodexNativeAuthController } from "./auth.js";
18
18
  import { injectPortableHistoryIntoCodex } from "./portable-history.js";
19
+ import { readCodexNativeProviderUsage } from "./provider-usage.js";
19
20
  import { CODEX_FIRST_USE_METADATA_KEY, CODEX_FIRST_USE_METADATA_VERSION, assertCodexNativeFirstUseMessageId, assertCodexNativeFirstUseDeliveryReceiptTurn, assertCodexNativePendingFirstUseRequest, assertCodexNativePendingFirstUseTurn, beginCodexNativeFirstUseAttempt, codexNativeFirstUseDeliveryReceipt, codexNativePendingFirstUseOwnerLiveness, endCodexNativeFirstUseAttempt, hashCanonicalCodexNativeFirstUsePrompt, isExactCodexNativeFirstUseDeliveryReplay, readCodexNativeFirstUseDeliveryReceipt, readCodexNativePendingFirstUse, } from "./first-use.js";
20
21
  export { CODEX_NATIVE_ADAPTER_ID } from "./thread.js";
21
22
  export const CODEX_NATIVE_ADAPTER_VERSION = "1.0.0";
@@ -490,6 +491,16 @@ export class CodexNativeThreadSession {
490
491
  ],
491
492
  };
492
493
  }
494
+ async getStatusSnapshot() {
495
+ const status = this.getStatus();
496
+ try {
497
+ const providerUsage = await readCodexNativeProviderUsage(this.process.client);
498
+ return providerUsage ? { ...status, providerUsage } : status;
499
+ }
500
+ catch {
501
+ return status;
502
+ }
503
+ }
493
504
  getNativeCompatibilityHandle() {
494
505
  return this.process.client;
495
506
  }
@@ -615,8 +626,10 @@ export class CodexNativeThreadSession {
615
626
  let reboundSettings = false;
616
627
  let phase = "stopping the previous process";
617
628
  try {
618
- await previousProcess.close();
629
+ await previousProcess.stop();
619
630
  this.resourceProcessUnavailable = true;
631
+ // Generation cleanup may race with exiting MCP children and must not block App Server replacement.
632
+ void previousProcess.close().catch(() => { });
620
633
  phase = "starting the replacement process";
621
634
  next = await this.reloadProcess();
622
635
  phase = "resuming the native thread";
@@ -393,11 +393,11 @@ function setBoundedPending(map, threadId, value) {
393
393
  map.set(threadId, value);
394
394
  }
395
395
  function parseContextUsage(value) {
396
- if (!isRecord(value.tokenUsage) || !isRecord(value.tokenUsage.total)) {
397
- throw new CodexNativeModelProtocolError("Codex token-usage notification is missing total usage.");
396
+ if (!isRecord(value.tokenUsage) || !isRecord(value.tokenUsage.last)) {
397
+ throw new CodexNativeModelProtocolError("Codex token-usage notification is missing last response usage.");
398
398
  }
399
- const total = value.tokenUsage.total;
400
- const tokens = requiredNonNegativeNumber(total.totalTokens, "total token usage");
399
+ const last = value.tokenUsage.last;
400
+ const tokens = requiredNonNegativeNumber(last.totalTokens, "last response token usage");
401
401
  const contextWindow = value.tokenUsage.modelContextWindow === undefined || value.tokenUsage.modelContextWindow === null
402
402
  ? undefined
403
403
  : requiredNonNegativeNumber(value.tokenUsage.modelContextWindow, "model context window");
@@ -6,6 +6,8 @@ import { protectPrivatePathsSync } from "../../core/private-path.js";
6
6
  import { CodexAppServerClient } from "./client.js";
7
7
  import { CODEX_APP_SERVER_PROTOCOL_NAME, CODEX_APP_SERVER_SUPPORTED_RANGE, CODEX_APP_SERVER_VERSION, } from "./protocol-version.js";
8
8
  const MAX_VERSION_OUTPUT_BYTES = 64 * 1024;
9
+ const SESSION_PATH_CLEANUP_MAX_RETRIES = 10;
10
+ const SESSION_PATH_CLEANUP_RETRY_DELAY_MS = 50;
9
11
  const PRIVATE_DIRECTORY_MODE = 0o700;
10
12
  const PRIVATE_FILE_MODE = 0o600;
11
13
  const INSTANCE_CONFIG = [
@@ -161,7 +163,12 @@ export async function prepareCodexNativeSessionPaths(input) {
161
163
  }
162
164
  }
163
165
  export async function disposeCodexNativeSessionPaths(paths) {
164
- await rm(paths.generationRoot, { recursive: true, force: true });
166
+ await rm(paths.generationRoot, {
167
+ recursive: true,
168
+ force: true,
169
+ maxRetries: SESSION_PATH_CLEANUP_MAX_RETRIES,
170
+ retryDelay: SESSION_PATH_CLEANUP_RETRY_DELAY_MS,
171
+ });
165
172
  await rmdir(paths.sessionRoot).catch((error) => {
166
173
  if (error.code !== "ENOTEMPTY" && error.code !== "ENOENT")
167
174
  throw error;
@@ -393,23 +400,35 @@ export async function diagnoseCodexNativeRuntime(config, runtimeInstanceId, opti
393
400
  export class CodexNativeAppServerProcess {
394
401
  client;
395
402
  paths;
396
- closePromise;
397
- constructor(client, paths) {
403
+ disposePaths;
404
+ stopPromise;
405
+ cleanupPromise;
406
+ pathsDisposed = false;
407
+ constructor(client, paths, disposePaths = disposeCodexNativeSessionPaths) {
398
408
  this.client = client;
399
409
  this.paths = paths;
410
+ this.disposePaths = disposePaths;
411
+ }
412
+ async stop() {
413
+ if (!this.stopPromise)
414
+ this.stopPromise = this.client.close();
415
+ await this.stopPromise;
400
416
  }
401
417
  async close() {
402
- if (!this.closePromise) {
403
- this.closePromise = (async () => {
404
- try {
405
- await this.client.close();
406
- }
407
- finally {
408
- await disposeCodexNativeSessionPaths(this.paths);
409
- }
410
- })();
418
+ await this.stop();
419
+ if (this.pathsDisposed)
420
+ return;
421
+ if (!this.cleanupPromise)
422
+ this.cleanupPromise = this.disposePaths(this.paths);
423
+ const cleanup = this.cleanupPromise;
424
+ try {
425
+ await cleanup;
426
+ this.pathsDisposed = true;
427
+ }
428
+ finally {
429
+ if (this.cleanupPromise === cleanup)
430
+ this.cleanupPromise = undefined;
411
431
  }
412
- await this.closePromise;
413
432
  }
414
433
  }
415
434
  export async function startCodexNativeAppServer(input) {
@@ -0,0 +1,115 @@
1
+ const PROVIDER_ID = "openai-codex";
2
+ const MAX_RATE_LIMIT_BUCKETS = 32;
3
+ const RATE_LIMIT_REQUEST_TIMEOUT_MS = 5_000;
4
+ function isRecord(value) {
5
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6
+ }
7
+ function optionalString(value, label) {
8
+ if (value === undefined || value === null)
9
+ return undefined;
10
+ if (typeof value !== "string")
11
+ throw new Error(`Codex ${label} is invalid.`);
12
+ return value;
13
+ }
14
+ function optionalNumber(value, label) {
15
+ if (value === undefined || value === null)
16
+ return undefined;
17
+ if (typeof value !== "number" || !Number.isFinite(value))
18
+ throw new Error(`Codex ${label} is invalid.`);
19
+ return value;
20
+ }
21
+ function formatWindowDuration(minutes, fallback) {
22
+ if (minutes === undefined || minutes <= 0)
23
+ return fallback;
24
+ if (minutes % (7 * 24 * 60) === 0)
25
+ return `${minutes / (7 * 24 * 60)}w`;
26
+ if (minutes % (24 * 60) === 0)
27
+ return `${minutes / (24 * 60)}d`;
28
+ if (minutes % 60 === 0)
29
+ return `${minutes / 60}h`;
30
+ return `${minutes}m`;
31
+ }
32
+ function normalizeWindow(value, fallbackLabel, prefix) {
33
+ if (value === undefined || value === null)
34
+ return undefined;
35
+ if (!isRecord(value))
36
+ throw new Error("Codex rate-limit window is invalid.");
37
+ const used = optionalNumber(value.usedPercent, "rate-limit percentage");
38
+ if (used === undefined)
39
+ throw new Error("Codex rate-limit percentage is missing.");
40
+ const usedPercent = Math.max(0, Math.min(100, used));
41
+ const duration = optionalNumber(value.windowDurationMins, "rate-limit duration");
42
+ const resetsAtSeconds = optionalNumber(value.resetsAt, "rate-limit reset time");
43
+ const windowLabel = `${formatWindowDuration(duration, fallbackLabel)} limit`;
44
+ return {
45
+ label: prefix ? `${prefix} ${windowLabel}` : windowLabel,
46
+ usedPercent,
47
+ remainingPercent: 100 - usedPercent,
48
+ ...(resetsAtSeconds !== undefined ? { resetsAt: new Date(resetsAtSeconds * 1_000).toISOString() } : {}),
49
+ };
50
+ }
51
+ function snapshotLabel(snapshot, fallback) {
52
+ return optionalString(snapshot.limitName, "rate-limit name") ?? fallback;
53
+ }
54
+ function snapshotLimits(snapshot, prefix) {
55
+ return [
56
+ normalizeWindow(snapshot.primary, "primary", prefix),
57
+ normalizeWindow(snapshot.secondary, "weekly", prefix),
58
+ ].filter((entry) => Boolean(entry));
59
+ }
60
+ function snapshotCredits(snapshot) {
61
+ if (snapshot.credits === undefined || snapshot.credits === null)
62
+ return undefined;
63
+ if (!isRecord(snapshot.credits) || typeof snapshot.credits.hasCredits !== "boolean" || typeof snapshot.credits.unlimited !== "boolean") {
64
+ throw new Error("Codex credit snapshot is invalid.");
65
+ }
66
+ if (!snapshot.credits.hasCredits)
67
+ return undefined;
68
+ return {
69
+ unlimited: snapshot.credits.unlimited,
70
+ balance: optionalString(snapshot.credits.balance, "credit balance"),
71
+ };
72
+ }
73
+ export function normalizeCodexNativeProviderUsage(value) {
74
+ if (!isRecord(value) || !isRecord(value.rateLimits))
75
+ throw new Error("Codex rate-limit response is invalid.");
76
+ const response = value;
77
+ const primary = response.rateLimits;
78
+ const limits = [...snapshotLimits(primary)];
79
+ let planType = optionalString(primary.planType, "plan type");
80
+ let credits = snapshotCredits(primary);
81
+ const primaryLimitId = optionalString(primary.limitId, "rate-limit id");
82
+ const additional = response.rateLimitsByLimitId;
83
+ if (additional !== undefined && additional !== null) {
84
+ if (!isRecord(additional))
85
+ throw new Error("Codex rate-limit bucket map is invalid.");
86
+ const entries = Object.entries(additional);
87
+ if (entries.length > MAX_RATE_LIMIT_BUCKETS)
88
+ throw new Error("Codex returned too many rate-limit buckets.");
89
+ for (const [limitId, rawSnapshot] of entries) {
90
+ if (limitId === primaryLimitId)
91
+ continue;
92
+ if (!isRecord(rawSnapshot))
93
+ throw new Error("Codex rate-limit bucket is invalid.");
94
+ const snapshot = rawSnapshot;
95
+ const prefix = snapshotLabel(snapshot, limitId);
96
+ limits.push(...snapshotLimits(snapshot, prefix));
97
+ planType ??= optionalString(snapshot.planType, "plan type");
98
+ credits ??= snapshotCredits(snapshot);
99
+ }
100
+ }
101
+ if (limits.length === 0 && !credits)
102
+ return undefined;
103
+ return {
104
+ provider: PROVIDER_ID,
105
+ ...(planType ? { planType } : {}),
106
+ limits,
107
+ ...(credits ? { credits } : {}),
108
+ };
109
+ }
110
+ export async function readCodexNativeProviderUsage(client) {
111
+ return normalizeCodexNativeProviderUsage(await client.request("account/rateLimits/read", null, {
112
+ timeoutMs: RATE_LIMIT_REQUEST_TIMEOUT_MS,
113
+ retryOverloaded: false,
114
+ }));
115
+ }
@@ -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
  }
@@ -1149,6 +1149,8 @@ export class RoutedSession {
1149
1149
  return {
1150
1150
  piboSessionId: this.piboSessionId,
1151
1151
  queuedMessages: this.queue.length,
1152
+ activeEventId: this.activeMessage?.id ?? this.activeExecutionEvent?.id,
1153
+ queuedEventIds: this.queue.map((item) => item.event.id ?? ""),
1152
1154
  processing: this.disposed ? false : this.processing,
1153
1155
  streaming: this.disposed ? false : this.runtime.session.isStreaming,
1154
1156
  activeTools: enabledTools,
@@ -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,12 +35,6 @@ 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;
@@ -112,7 +124,7 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
112
124
  if (row.type === "compaction_start")
113
125
  return { ...base, type: "compaction_start", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown" };
114
126
  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") };
127
+ 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
128
  if (row.type === "session_error") {
117
129
  const error = stringAttribute(attributes, "error") ?? row.preview_text ?? "Error";
118
130
  return { ...base, type: "session_error", error, errorDetails: normalizeSessionErrorDetails(error, isRecord(attributes.errorDetails) ? attributes.errorDetails : undefined) };
@@ -121,14 +133,14 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
121
133
  return { type: "user.message.accepted", piboSessionId, roomId: row.room_id ?? undefined, text: stringAttribute(attributes, "inlineText") ?? row.preview_text ?? "", clientTxnId: stringAttribute(attributes, "clientTxnId") };
122
134
  return { ...base, type: row.type };
123
135
  }
124
- function readPersistedPayload(row, payloadStore) {
136
+ function readPersistedPayload(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
125
137
  if (!row.payload_ref || !payloadStore)
126
138
  return undefined;
127
139
  try {
128
140
  const metadata = payloadStore.getPayload(row.payload_ref);
129
- if (!metadata || metadata.byteSize > MAX_TRACE_EVENT_HYDRATION_BYTES)
141
+ if (!metadata || metadata.byteSize > hydrationBytes)
130
142
  return undefined;
131
- const text = Buffer.from(payloadStore.readPayloadBytesBounded(row.payload_ref, MAX_TRACE_EVENT_HYDRATION_BYTES)).toString("utf8");
143
+ const text = Buffer.from(payloadStore.readPayloadBytesBounded(row.payload_ref, hydrationBytes)).toString("utf8");
132
144
  return metadata.contentType.includes("json") ? JSON.parse(text) : text;
133
145
  }
134
146
  catch {
@@ -136,6 +148,13 @@ function readPersistedPayload(row, payloadStore) {
136
148
  }
137
149
  }
138
150
  function tracePayloadIdentityForEvent(row, attributes) {
151
+ const semanticId = stringAttribute(attributes, "semanticEventId") ?? row.event_id;
152
+ if (semanticId && (row.type === "assistant_message" || row.type === "thinking_finished")) {
153
+ const thinking = row.type === "thinking_finished";
154
+ const index = numberAttribute(attributes, thinking ? "thinkingIndex" : "assistantIndex") ?? numberAttribute(attributes, "contentIndex");
155
+ const identity = index === undefined ? semanticId : `${semanticId}:${thinking ? "thinking" : "assistant"}:${index}`;
156
+ return { nodeId: `${thinking ? "reasoning" : "assistant"}:${identity}`, payloadKind: thinking ? "reasoning" : "output" };
157
+ }
139
158
  const toolCallId = stringAttribute(attributes, "toolCallId");
140
159
  if (!toolCallId)
141
160
  return undefined;