@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
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#101d22" />
7
7
  <title>Pibo</title>
8
- <script type="module" crossorigin src="/apps/chat-vscode/assets/index-CMwTB8o8.js"></script>
8
+ <script type="module" crossorigin src="/apps/chat-vscode/assets/index-DZgW1fCB.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-b18ZkEo0.css">
10
10
  </head>
11
11
  <body>
@@ -641,7 +641,7 @@ export class LocalCliSessionSource {
641
641
  if (delivery.persisted)
642
642
  continue;
643
643
  try {
644
- this.ingestOutputEvent(session, delivery.event);
644
+ this.ingestOutputEvent(session, delivery.event, retryContext.attempt > 1 ? "durable-replay" : "live");
645
645
  }
646
646
  catch (error) {
647
647
  errors.push(error instanceof Error ? error : new Error(String(error)));
@@ -753,7 +753,7 @@ export class LocalCliSessionSource {
753
753
  // Persistence is best-effort; live local rendering still proceeds through router events.
754
754
  }
755
755
  }
756
- ingestOutputEvent(session, event) {
756
+ ingestOutputEvent(session, event, phase) {
757
757
  if (!this.ingestService)
758
758
  return;
759
759
  this.ingestService.ingestOutputEvent({
@@ -762,6 +762,7 @@ export class LocalCliSessionSource {
762
762
  actorId: session.profile,
763
763
  event,
764
764
  createdAt: this.now(),
765
+ persistenceProvenance: { producer: "local-cli", projection: "product-history", phase },
765
766
  });
766
767
  }
767
768
  buildStatus(session) {
@@ -68,7 +68,7 @@ export async function prepareDeploymentSeed(input) {
68
68
  await copySeedNonDatabaseFiles(input.config.seedSourceHome, homePath, input.mode, input.config.root);
69
69
  const copiedDatabases = await backupSeedDatabases(input.config.seedSourceHome, homePath, input.mode);
70
70
  if (input.config.seedSourcePiHome && existsSync(input.config.seedSourcePiHome)) {
71
- await copyPiRuntimeAuth(input.config.seedSourcePiHome, piHomePath);
71
+ await copyPiRuntimeSeed(input.config.seedSourcePiHome, piHomePath);
72
72
  }
73
73
  if (input.mode === "full" && input.config.seedSourceWorkspace && existsSync(input.config.seedSourceWorkspace)) {
74
74
  await cp(input.config.seedSourceWorkspace, workspacePath, { recursive: true, force: true, preserveTimestamps: true });
@@ -160,11 +160,33 @@ async function backupSeedDatabases(sourceHome, destinationHome, mode) {
160
160
  }
161
161
  return copied.sort();
162
162
  }
163
- async function copyPiRuntimeAuth(sourcePiHome, destinationPiHome) {
163
+ async function copyPiRuntimeSeed(sourcePiHome, destinationPiHome) {
164
164
  const sourceAgent = resolve(sourcePiHome, "agent");
165
165
  const destinationAgent = resolve(destinationPiHome, "agent");
166
166
  await mkdir(destinationAgent, { recursive: true, mode: 0o700 });
167
- for (const name of ["auth.json", "models-store.json"]) {
167
+ // OAuth refresh tokens belong to one credential store. Cloning them into isolated
168
+ // slots makes token rotation race with the source login and other slots.
169
+ const authPath = resolve(sourceAgent, "auth.json");
170
+ if (existsSync(authPath)) {
171
+ let parsed;
172
+ try {
173
+ parsed = JSON.parse(await readFile(authPath, "utf8"));
174
+ }
175
+ catch {
176
+ throw new Error("Cannot read the Pi credential seed.");
177
+ }
178
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
179
+ throw new Error("Invalid Pi credential seed.");
180
+ const apiKeys = Object.fromEntries(Object.entries(parsed).flatMap(([provider, value]) => {
181
+ if (!value || typeof value !== "object" || value.type !== "api_key" || typeof value.key !== "string")
182
+ return [];
183
+ return [[provider, { type: "api_key", key: value.key }]];
184
+ }));
185
+ if (Object.keys(apiKeys).length) {
186
+ await writeFile(resolve(destinationAgent, "auth.json"), `${JSON.stringify(apiKeys)}\n`, { mode: 0o600 });
187
+ }
188
+ }
189
+ for (const name of ["models-store.json"]) {
168
190
  const source = resolve(sourceAgent, name);
169
191
  if (!existsSync(source))
170
192
  continue;
@@ -4,3 +4,7 @@ export class PiboSteeringUnavailableError extends Error {
4
4
  this.name = "PiboSteeringUnavailableError";
5
5
  }
6
6
  }
7
+ /** Only queue-clear actions can carry an already-completed ingress count. */
8
+ export function previouslyClearedMessages(event) {
9
+ return event.action === "clear_queue" && Number.isSafeInteger(event.clearedBeforeRuntime) && event.clearedBeforeRuntime > 0 ? event.clearedBeforeRuntime : 0;
10
+ }
@@ -109,9 +109,10 @@ export class OutputRenderSequencer {
109
109
  const suppliedPart = transition.suppliedIndex === undefined
110
110
  ? undefined
111
111
  : parts.find((part) => part.index === transition.suppliedIndex);
112
- if (transition.suppliedIndex !== undefined && (transition.canonicalIndex || suppliedPart)) {
112
+ if (transition.suppliedIndex !== undefined && (transition.canonicalIndex
113
+ || (suppliedPart && (!suppliedPart.closed || suppliedPart.identityFingerprint === transition.identityFingerprint)))) {
113
114
  this.highWaterStore?.observeOutputPart?.({ ...transition, index: transition.suppliedIndex });
114
- this.recordOutputPart(parts, transition.suppliedIndex, transition.terminal);
115
+ this.recordOutputPart(parts, transition.suppliedIndex, transition.terminal, transition.identityFingerprint);
115
116
  this.trimOutputParts(state);
116
117
  return event;
117
118
  }
@@ -122,18 +123,20 @@ export class OutputRenderSequencer {
122
123
  const index = latestOpen
123
124
  ? localIndex
124
125
  : this.highWaterStore?.claimOrAttachOutputPart?.({ ...transition, proposedIndex: localIndex }) ?? localIndex;
125
- this.recordOutputPart(parts, index, transition.terminal);
126
+ this.recordOutputPart(parts, index, transition.terminal, transition.identityFingerprint);
126
127
  this.trimOutputParts(state);
127
128
  return withOutputPartIndex(event, transition.kind, index);
128
129
  }
129
- recordOutputPart(parts, index, terminal) {
130
+ recordOutputPart(parts, index, terminal, identityFingerprint) {
130
131
  let part = parts.find((candidate) => candidate.index === index);
131
132
  if (!part) {
132
- part = { index, closed: false };
133
+ part = { index, closed: false, identityFingerprint };
133
134
  parts.push(part);
134
135
  }
135
- if (terminal)
136
+ if (terminal) {
136
137
  part.closed = true;
138
+ part.identityFingerprint = identityFingerprint;
139
+ }
137
140
  }
138
141
  trimOutputParts(state) {
139
142
  let total = 0;
@@ -380,11 +383,66 @@ function outputPartTransition(event, activeEventId) {
380
383
  function validOutputPartIndex(value) {
381
384
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
382
385
  }
386
+ export const OUTPUT_IDENTITY_FINGERPRINT_VERSION = 2;
383
387
  export function outputIdentityFingerprint(event) {
388
+ return createHash("sha256").update(stableJson(outputIdentityPayload(event))).digest("hex");
389
+ }
390
+ /** Exact v1 algorithm retained only for comparing pre-v2 persisted fingerprints. */
391
+ export function legacyOutputIdentityFingerprint(event) {
384
392
  const payload = { ...event };
385
393
  delete payload.renderSequence;
394
+ delete payload.compactionStats;
386
395
  return createHash("sha256").update(stableJson(payload)).digest("hex");
387
396
  }
397
+ export function legacyOutputIdentityFingerprintCandidates(event) {
398
+ const variants = [{ ...event }];
399
+ const assistant = event.type === "assistant_delta" || event.type === "assistant_message";
400
+ const thinking = event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished";
401
+ const index = assistant ? event.assistantIndex ?? event.contentIndex : thinking ? event.thinkingIndex ?? event.contentIndex : undefined;
402
+ if (index !== undefined && (assistant || thinking)) {
403
+ for (const attribute of assistant ? ["assistantIndex", "contentIndex"] : ["thinkingIndex", "contentIndex"]) {
404
+ const variant = { ...event };
405
+ delete variant.assistantIndex;
406
+ delete variant.thinkingIndex;
407
+ delete variant.contentIndex;
408
+ variant[attribute] = index;
409
+ variants.push(variant);
410
+ }
411
+ }
412
+ for (const variant of [...variants]) {
413
+ if ("provenance" in variant) {
414
+ const withoutProvenance = { ...variant };
415
+ delete withoutProvenance.provenance;
416
+ variants.push(withoutProvenance);
417
+ }
418
+ }
419
+ return [...new Set(variants.map((variant) => legacyOutputIdentityFingerprint(variant)))];
420
+ }
421
+ /** Redacted, bounded evidence for diagnosing a fingerprint mismatch without retaining values. */
422
+ export function outputIdentityFieldDigests(event) {
423
+ return Object.fromEntries(Object.entries(outputIdentityPayload(event))
424
+ .slice(0, 64)
425
+ .map(([field, value]) => [field, createHash("sha256").update(stableJson(value)).digest("hex").slice(0, 16)]));
426
+ }
427
+ function outputIdentityPayload(event) {
428
+ const payload = { ...event };
429
+ delete payload.renderSequence;
430
+ delete payload.compactionStats;
431
+ // Delivery provenance describes how equivalent output reached persistence; it
432
+ // is diagnostic metadata rather than semantic message content.
433
+ delete payload.provenance;
434
+ if (event.type === "assistant_delta" || event.type === "assistant_message") {
435
+ delete payload.assistantIndex;
436
+ delete payload.contentIndex;
437
+ payload.outputPartIndex = event.assistantIndex ?? event.contentIndex ?? 0;
438
+ }
439
+ else if (event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished") {
440
+ delete payload.thinkingIndex;
441
+ delete payload.contentIndex;
442
+ payload.outputPartIndex = event.thinkingIndex ?? event.contentIndex ?? 0;
443
+ }
444
+ return payload;
445
+ }
388
446
  export function outputPartFingerprint(event) {
389
447
  const payload = { ...event };
390
448
  delete payload.renderSequence;
@@ -393,6 +451,7 @@ export function outputPartFingerprint(event) {
393
451
  delete payload.usageIndex;
394
452
  delete payload.compactionIndex;
395
453
  delete payload.contentIndex;
454
+ delete payload.compactionStats;
396
455
  return createHash("sha256").update(stableJson(payload)).digest("hex");
397
456
  }
398
457
  function stableJson(value) {
@@ -0,0 +1,33 @@
1
+ /** Pi's request boundary releases before tool work, so parents cannot pin slots while awaiting children. */
2
+ export function createProviderCapacityExtension(capacity, room, nested = false) {
3
+ return pi => {
4
+ let lease;
5
+ let removeAbort;
6
+ const release = () => { removeAbort?.(); removeAbort = undefined; lease?.release(); lease = undefined; };
7
+ pi.on("before_provider_request", async (_event, ctx) => {
8
+ // Retries replace a previous failed HTTP request, never overlap its reservation.
9
+ release();
10
+ const signal = ctx.signal;
11
+ try {
12
+ lease = await capacity.acquireProvider(ctx.model?.provider ?? "pi-unknown", room, signal, nested);
13
+ if (signal?.aborted) {
14
+ release();
15
+ return;
16
+ }
17
+ signal?.addEventListener("abort", release, { once: true });
18
+ removeAbort = () => signal?.removeEventListener("abort", release);
19
+ }
20
+ catch (error) {
21
+ // Extension errors are observational in Pi. Abort explicitly so a rejected wait cannot send anyway.
22
+ ctx.abort();
23
+ throw error;
24
+ }
25
+ });
26
+ pi.on("after_provider_response", event => { if (event.status >= 400)
27
+ release(); });
28
+ pi.on("message_end", event => { if (event.message.role === "assistant")
29
+ release(); });
30
+ pi.on("agent_end", release);
31
+ pi.on("session_shutdown", release);
32
+ };
33
+ }
@@ -11,7 +11,8 @@ export class PiboProviderTelemetryRecorder {
11
11
  }
12
12
  recordRequestStart(payload, options = {}) {
13
13
  const capturedOptions = { ...options, at: options.at ?? new Date().toISOString() };
14
- return this.schedule(() => this.recordRequestStartNow(providerPayloadSnapshot(payload), capturedOptions));
14
+ const captured = providerPayloadSnapshot(payload);
15
+ return this.schedule({ kind: "start", payload: captured, options: capturedOptions }, () => this.recordRequestStartNow(captured, capturedOptions));
15
16
  }
16
17
  recordRequestStartNow(payload, options) {
17
18
  if (!this.options.store)
@@ -79,7 +80,7 @@ export class PiboProviderTelemetryRecorder {
79
80
  }
80
81
  recordResponse(input) {
81
82
  const captured = { status: input.status, at: input.at ?? new Date().toISOString() };
82
- return this.schedule(() => this.recordResponseNow(captured));
83
+ return this.schedule({ kind: "response", input: captured }, () => this.recordResponseNow(captured));
83
84
  }
84
85
  recordResponseNow(input) {
85
86
  if (!this.options.store)
@@ -141,7 +142,7 @@ export class PiboProviderTelemetryRecorder {
141
142
  return undefined;
142
143
  const captured = providerAssistantMessageSnapshot(message);
143
144
  const capturedOptions = { ...options, at: options.at ?? new Date().toISOString() };
144
- return this.schedule(() => this.recordMessageEndNow(captured, capturedOptions));
145
+ return this.schedule({ kind: "end", message: captured, options: capturedOptions }, () => this.recordMessageEndNow(captured, capturedOptions));
145
146
  }
146
147
  recordMessageEndNow(message, options) {
147
148
  if (!this.options.store)
@@ -159,11 +160,25 @@ export class PiboProviderTelemetryRecorder {
159
160
  return this.finishActiveProviderRequest(status, options.at ?? new Date().toISOString(), summary, errorMessage, errorDetails?.category ?? errorDetails?.errorClass);
160
161
  }
161
162
  recordShutdown(reason, at = new Date().toISOString()) {
162
- return this.schedule(() => this.finishActiveProviderRequest("aborted", at, reason, undefined, "runtime_abort"));
163
+ return this.schedule({ kind: "shutdown", at, reason }, () => this.finishActiveProviderRequest("aborted", at, reason, undefined, "runtime_abort"));
163
164
  }
164
- schedule(write) {
165
+ executeTelemetryCommand(command) {
166
+ switch (command.kind) {
167
+ case "start":
168
+ this.recordRequestStartNow(command.payload, command.options);
169
+ break;
170
+ case "response":
171
+ this.recordResponseNow(command.input);
172
+ break;
173
+ case "end":
174
+ this.recordMessageEndNow(command.message, command.options);
175
+ break;
176
+ case "shutdown": this.finishActiveProviderRequest("aborted", command.at, command.reason, undefined, "runtime_abort");
177
+ }
178
+ }
179
+ schedule(command, write) {
165
180
  if (this.options.writer) {
166
- this.options.writer.enqueue(write, this.options.onError);
181
+ this.options.writer.record({ recorder: "provider", command, session: { ...this.options.session, metadata: { chatRoomId: this.options.session.metadata?.chatRoomId ?? null, rootSessionId: this.options.session.metadata?.rootSessionId ?? null } }, model: this.options.model }, write, this.options.onError);
167
182
  return undefined;
168
183
  }
169
184
  return write();
@@ -0,0 +1,174 @@
1
+ export function resolveRuntimeCapacityOptions(env = process.env) {
2
+ const read = (name, fallback) => {
3
+ if (!env[name]?.trim())
4
+ return fallback;
5
+ const n = Number(env[name]);
6
+ if (!Number.isSafeInteger(n) || n < 1)
7
+ throw new Error(`${name} must be a positive integer.`);
8
+ return n;
9
+ };
10
+ return {
11
+ coldStarts: read("PIBO_GATEWAY_MAX_COLD_STARTS", 2), providerTurns: read("PIBO_GATEWAY_MAX_PROVIDER_TURNS", 10),
12
+ providerTurnsPerRoom: read("PIBO_GATEWAY_MAX_PROVIDER_TURNS_PER_ROOM", 5), maxWaiting: read("PIBO_GATEWAY_MAX_RUNTIME_WAITERS", 64),
13
+ maxWaitMs: read("PIBO_GATEWAY_MAX_RUNTIME_WAIT_MS", 60_000), maxRuntimes: read("PIBO_GATEWAY_MAX_ACTIVE_RUNTIMES", 32),
14
+ };
15
+ }
16
+ /** Bounded room round-robin. Cancellation removes a waiter without consuming a slot. */
17
+ export class FairCapacityPool {
18
+ limit;
19
+ perRoom;
20
+ maxWaiting;
21
+ maxWaitMs;
22
+ reserved;
23
+ rooms = new Map();
24
+ activeRooms = new Map();
25
+ active = 0;
26
+ generalActive = 0;
27
+ generalRooms = new Map();
28
+ waiting = 0;
29
+ closed = false;
30
+ constructor(limit, perRoom, maxWaiting, maxWaitMs, reserved = 0) {
31
+ this.limit = limit;
32
+ this.perRoom = perRoom;
33
+ this.maxWaiting = maxWaiting;
34
+ this.maxWaitMs = maxWaitMs;
35
+ this.reserved = reserved;
36
+ if (!Number.isSafeInteger(reserved) || reserved < 0 || reserved >= limit)
37
+ throw new Error("Reserved capacity must leave at least one normal slot.");
38
+ for (const n of [limit, perRoom, maxWaiting, maxWaitMs])
39
+ if (!Number.isSafeInteger(n) || n < 1)
40
+ throw new Error("Capacity limits must be positive integers.");
41
+ }
42
+ acquire(room, signal, reserved = false) {
43
+ if (this.closed)
44
+ return Promise.reject(capacityError("Runtime capacity is closed."));
45
+ if (signal?.aborted)
46
+ return Promise.reject(signal.reason ?? capacityError("Capacity wait cancelled."));
47
+ if (this.waiting >= this.maxWaiting - (reserved ? 0 : Math.min(this.reserved, this.maxWaiting - 1)))
48
+ return Promise.reject(capacityError("Runtime capacity wait queue is full."));
49
+ return new Promise((resolve, reject) => {
50
+ let timer;
51
+ const cancel = () => {
52
+ const queue = this.rooms.get(room);
53
+ const index = queue?.indexOf(waiter) ?? -1;
54
+ if (index < 0)
55
+ return;
56
+ queue.splice(index, 1);
57
+ this.waiting--;
58
+ if (!queue.length)
59
+ this.rooms.delete(room);
60
+ waiter.cleanup();
61
+ reject(signal?.aborted ? signal.reason : capacityError("Runtime capacity wait deadline exceeded."));
62
+ this.drain();
63
+ };
64
+ const waiter = { room, reserved, queuedAt: Date.now(), resolve, reject, cleanup: () => { clearTimeout(timer); signal?.removeEventListener("abort", cancel); } };
65
+ const queue = this.rooms.get(room) ?? [];
66
+ queue.push(waiter);
67
+ this.rooms.set(room, queue);
68
+ this.waiting++;
69
+ timer = setTimeout(cancel, this.maxWaitMs);
70
+ timer.unref?.();
71
+ signal?.addEventListener("abort", cancel, { once: true });
72
+ this.drain();
73
+ });
74
+ }
75
+ snapshot() {
76
+ let oldest = 0;
77
+ for (const queue of this.rooms.values())
78
+ oldest = Math.max(oldest, Date.now() - queue[0].queuedAt);
79
+ return { active: this.active, waiting: this.waiting, limit: this.limit, perRoom: this.perRoom, reserved: this.reserved, maxWaiting: this.maxWaiting, maxWaitMs: this.maxWaitMs, oldestWaitMs: oldest };
80
+ }
81
+ close() {
82
+ this.closed = true;
83
+ for (const queue of this.rooms.values())
84
+ for (const waiter of queue) {
85
+ waiter.cleanup();
86
+ waiter.reject(capacityError("Runtime capacity is closed."));
87
+ }
88
+ this.rooms.clear();
89
+ this.waiting = 0;
90
+ }
91
+ drain() {
92
+ while (!this.closed && this.active < this.limit) {
93
+ const eligible = (waiter) => (this.activeRooms.get(waiter.room) ?? 0) < this.perRoom && (waiter.reserved ||
94
+ (this.generalActive < this.limit - this.reserved && (this.generalRooms.get(waiter.room) ?? 0) < this.perRoom - Math.min(this.reserved, 1, this.perRoom - 1)));
95
+ const entry = [...this.rooms].find(([, queue]) => queue.some(eligible));
96
+ if (!entry)
97
+ return;
98
+ const [room, queue] = entry;
99
+ const [waiter] = queue.splice(queue.findIndex(eligible), 1);
100
+ this.rooms.delete(room);
101
+ if (queue.length)
102
+ this.rooms.set(room, queue);
103
+ if (!waiter.reserved) {
104
+ this.generalActive++;
105
+ this.generalRooms.set(room, (this.generalRooms.get(room) ?? 0) + 1);
106
+ }
107
+ this.waiting--;
108
+ this.active++;
109
+ this.activeRooms.set(room, (this.activeRooms.get(room) ?? 0) + 1);
110
+ waiter.cleanup();
111
+ let released = false;
112
+ waiter.resolve({ release: () => {
113
+ if (released)
114
+ return;
115
+ released = true;
116
+ this.active--;
117
+ if (!waiter.reserved) {
118
+ this.generalActive--;
119
+ const general = this.generalRooms.get(room) - 1;
120
+ if (general)
121
+ this.generalRooms.set(room, general);
122
+ else
123
+ this.generalRooms.delete(room);
124
+ }
125
+ const count = this.activeRooms.get(room) - 1;
126
+ if (count)
127
+ this.activeRooms.set(room, count);
128
+ else
129
+ this.activeRooms.delete(room);
130
+ this.drain();
131
+ } });
132
+ }
133
+ }
134
+ }
135
+ export class RuntimeCapacity {
136
+ starts;
137
+ maxRuntimes;
138
+ providers = new Map();
139
+ closed = false;
140
+ options;
141
+ constructor(options = {}) {
142
+ this.options = { ...resolveRuntimeCapacityOptions(), ...options };
143
+ for (const n of Object.values(this.options))
144
+ if (!Number.isSafeInteger(n) || n < 1)
145
+ throw new Error("Runtime capacity limits must be positive integers.");
146
+ this.maxRuntimes = this.options.maxRuntimes;
147
+ this.starts = new FairCapacityPool(this.options.coldStarts, 1, this.options.maxWaiting, this.options.maxWaitMs);
148
+ }
149
+ async acquireProvider(provider, room, signal, nested = false) {
150
+ if (this.closed)
151
+ throw capacityError("Runtime capacity is closed.");
152
+ let pool = this.providers.get(provider);
153
+ if (!pool) {
154
+ if (this.providers.size >= 64)
155
+ throw capacityError("Active provider capacity registry is full.");
156
+ pool = new FairCapacityPool(this.options.providerTurns, this.options.providerTurnsPerRoom, this.options.maxWaiting, this.options.maxWaitMs, Math.min(2, this.options.providerTurns - 1));
157
+ this.providers.set(provider, pool);
158
+ }
159
+ try {
160
+ const lease = await pool.acquire(room, signal, nested);
161
+ return { release: () => { lease.release(); this.removeIdleProvider(provider, pool); } };
162
+ }
163
+ catch (error) {
164
+ this.removeIdleProvider(provider, pool);
165
+ throw error;
166
+ }
167
+ }
168
+ snapshot() { return { coldStarts: this.starts.snapshot(), maxRuntimes: this.maxRuntimes, providers: [...this.providers].map(([provider, pool]) => ({ provider, ...pool.snapshot() })) }; }
169
+ close() { this.closed = true; this.starts.close(); for (const pool of this.providers.values())
170
+ pool.close(); this.providers.clear(); }
171
+ removeIdleProvider(provider, pool) { const state = pool.snapshot(); if (!state.active && !state.waiting && this.providers.get(provider) === pool)
172
+ this.providers.delete(provider); }
173
+ }
174
+ function capacityError(message) { return Object.assign(new Error(message), { code: "runtime_capacity_unavailable" }); }
@@ -1,5 +1,13 @@
1
1
  import { BestEffortTelemetryService, } from "../data/telemetry.js";
2
2
  import { isTerminalProviderStatus } from "./provider-telemetry.js";
3
+ /** Diagnostic cache only: terminal events clear their keys; the cap also contains orphaned/incomplete histories. */
4
+ class TelemetryProgressMap extends Map {
5
+ set(key, value) {
6
+ if (!this.has(key) && this.size >= 1024)
7
+ this.delete(this.keys().next().value);
8
+ return super.set(key, value);
9
+ }
10
+ }
3
11
  const TERMINAL_TURN_STATUSES = new Set(["ok", "error", "aborted", "timeout"]);
4
12
  const DEFAULT_PROGRESS_FLUSH_INTERVAL_MS = 1_000;
5
13
  export class PiboRuntimeTelemetryRecorder {
@@ -9,10 +17,11 @@ export class PiboRuntimeTelemetryRecorder {
9
17
  providerEventMode;
10
18
  progressFlushIntervalMs;
11
19
  writer;
12
- pendingProviderProgress = new Map();
13
- providerRequestCache = new Map();
14
- lastProviderFlushAtMs = new Map();
15
- lastProgressWriteAtMs = new Map();
20
+ phaseSequences = new TelemetryProgressMap();
21
+ pendingProviderProgress = new TelemetryProgressMap();
22
+ providerRequestCache = new TelemetryProgressMap();
23
+ lastProviderFlushAtMs = new TelemetryProgressMap();
24
+ lastProgressWriteAtMs = new TelemetryProgressMap();
16
25
  constructor(store, onError, options = {}) {
17
26
  this.store = store;
18
27
  this.onError = onError;
@@ -29,7 +38,7 @@ export class PiboRuntimeTelemetryRecorder {
29
38
  return;
30
39
  const captured = captureTelemetryContext(context);
31
40
  const capturedEvent = telemetryOutputEventSnapshot(event);
32
- this.schedule(() => this.recordOutputUnsafe(capturedEvent, captured));
41
+ this.schedule({ kind: "output", event: capturedEvent, context: captured }, () => this.recordOutputUnsafe(capturedEvent, captured));
33
42
  }
34
43
  recordPiEvent(piboSessionId, event, context = {}) {
35
44
  if (!this.store)
@@ -38,20 +47,29 @@ export class PiboRuntimeTelemetryRecorder {
38
47
  if (!summary)
39
48
  return;
40
49
  const captured = captureTelemetryContext(context);
41
- this.schedule(() => this.recordPiEventSummaryUnsafe(piboSessionId, summary, captured));
50
+ this.schedule({ kind: "pi", piboSessionId, summary, context: captured }, () => this.recordPiEventSummaryUnsafe(piboSessionId, summary, captured));
42
51
  }
43
52
  recordMessagesInterrupted(messages, context = {}, reason = "message interrupted") {
44
53
  if (!this.store)
45
54
  return;
46
55
  const captured = captureTelemetryContext(context);
47
56
  const interrupted = messages.flatMap((message) => message.id ? [{ piboSessionId: message.piboSessionId, eventId: message.id }] : []);
48
- this.schedule(() => {
57
+ this.schedule({ kind: "interrupted", messages: interrupted, context: captured, reason }, () => {
49
58
  for (const message of interrupted) {
50
59
  this.recordTurnTerminal(message, captured, "aborted", "abort", reason, "runtime_abort");
51
60
  }
52
61
  });
53
62
  }
54
- schedule(write) {
63
+ executeTelemetryCommand(command) {
64
+ if (command.kind === "output")
65
+ this.recordOutputUnsafe(command.event, command.context);
66
+ else if (command.kind === "pi")
67
+ this.recordPiEventSummaryUnsafe(command.piboSessionId, command.summary, command.context);
68
+ else
69
+ for (const message of command.messages)
70
+ this.recordTurnTerminal(message, command.context, "aborted", "abort", command.reason, "runtime_abort");
71
+ }
72
+ schedule(command, write) {
55
73
  const guarded = () => {
56
74
  try {
57
75
  write();
@@ -61,7 +79,7 @@ export class PiboRuntimeTelemetryRecorder {
61
79
  }
62
80
  };
63
81
  if (this.writer)
64
- this.writer.enqueue(guarded, this.onError);
82
+ this.writer.record({ recorder: "runtime", command, providerEventMode: this.providerEventMode, progressFlushIntervalMs: this.progressFlushIntervalMs }, guarded, this.onError);
65
83
  else
66
84
  guarded();
67
85
  }
@@ -581,6 +599,10 @@ export class PiboRuntimeTelemetryRecorder {
581
599
  counters: options.counters,
582
600
  summary,
583
601
  });
602
+ if (phase && !existing) {
603
+ const key = phaseId(turn.turnId, phaseName);
604
+ this.phaseSequences.set(key, (this.phaseSequences.get(key) ?? 0) + 1);
605
+ }
584
606
  if (options.updateTurn) {
585
607
  this.telemetry.upsertTurn({
586
608
  turnId: turn.turnId,
@@ -602,7 +624,8 @@ export class PiboRuntimeTelemetryRecorder {
602
624
  }
603
625
  nextPhaseId(turnId, phaseName) {
604
626
  const base = phaseId(turnId, phaseName);
605
- const count = this.store?.countPhasesForTurn(turnId, phaseName) ?? 0;
627
+ const count = this.phaseSequences.get(base) ?? this.store?.countPhasesForTurn(turnId, phaseName) ?? 0;
628
+ this.phaseSequences.set(base, count);
606
629
  if (count === 0)
607
630
  return base;
608
631
  return `${base}:${count + 1}`;
@@ -718,6 +741,9 @@ export class PiboRuntimeTelemetryRecorder {
718
741
  this.lastProviderFlushAtMs.delete(turnId);
719
742
  }
720
743
  clearTurnProgress(turnId) {
744
+ for (const key of this.phaseSequences.keys())
745
+ if (key.startsWith(`${turnId}:`))
746
+ this.phaseSequences.delete(key);
721
747
  this.clearProviderProgress(turnId);
722
748
  const prefix = `${turnId}:`;
723
749
  for (const key of this.lastProgressWriteAtMs.keys()) {
@@ -1069,6 +1095,8 @@ function telemetryOutputEventSnapshot(event) {
1069
1095
  return { ...event, result: event.isError ? safeErrorMessage(event.result) : undefined };
1070
1096
  if (event.type === "execution_result")
1071
1097
  return { ...event, result: undefined };
1098
+ if (event.type === "assistant_delta" || event.type === "thinking_delta" || event.type === "assistant_message" || event.type === "message_queued" || event.type === "message_started")
1099
+ return { ...event, text: "" };
1072
1100
  return { ...event };
1073
1101
  }
1074
1102
  function captureTelemetryContext(context) {
@@ -1076,8 +1104,8 @@ function captureTelemetryContext(context) {
1076
1104
  const atMs = context.atMs ?? (Number.isFinite(parsedAtMs) ? parsedAtMs : Date.now());
1077
1105
  return {
1078
1106
  ...context,
1079
- session: context.session ? { ...context.session, metadata: context.session.metadata ? { ...context.session.metadata } : undefined } : undefined,
1080
- status: context.status ? { ...context.status, activeTools: [...context.status.activeTools], enabledTools: [...context.status.enabledTools] } : undefined,
1107
+ session: context.session ? { ...context.session, metadata: context.session.metadata ? { chatRoomId: context.session.metadata.chatRoomId, rootSessionId: context.session.metadata.rootSessionId } : undefined } : undefined,
1108
+ status: context.status ? { ...context.status, activeTools: [], enabledTools: [] } : undefined,
1081
1109
  at: context.at ?? new Date(atMs).toISOString(),
1082
1110
  atMs,
1083
1111
  };