@pasko70/pibo 3.4.4 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/agent-runtime/routed-session.js +37 -12
  2. package/dist/agent-runtimes/pi/adapter.js +1 -0
  3. package/dist/agent-runtimes/pi/routed-session.js +2 -2
  4. package/dist/apps/chat/bounded-event-stream.js +98 -0
  5. package/dist/apps/chat/chat-settings-routes.js +3 -3
  6. package/dist/apps/chat/data/chat-data-mappers.js +32 -13
  7. package/dist/apps/chat/data/event-command-service.js +30 -21
  8. package/dist/apps/chat/data/history-query-service.js +34 -25
  9. package/dist/apps/chat/data/read-state-service.js +13 -0
  10. package/dist/apps/chat/data/session-query-service.js +19 -11
  11. package/dist/apps/chat/data/timeline-query-service.js +19 -7
  12. package/dist/apps/chat/message-command-dispatcher.js +134 -0
  13. package/dist/apps/chat/output-compactor.js +9 -0
  14. package/dist/apps/chat/output-event-policy.js +9 -1
  15. package/dist/apps/chat/stream.js +21 -3
  16. package/dist/apps/chat/telemetry-retention-service.js +113 -8
  17. package/dist/apps/chat/trace-response-cache.js +46 -0
  18. package/dist/apps/chat/trace-v2.js +12 -6
  19. package/dist/apps/chat/trace.js +1 -1
  20. package/dist/apps/chat/web-app.js +608 -281
  21. package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-BG0n7zLd.js} +1 -1
  22. package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-D6TjFhAm.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-D79vyxSX.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-DFZ8cwh0.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-cOjokPrK.js} +1 -1
  26. package/dist/apps/chat-ui/assets/index-RMHUTJ62.js +229 -0
  27. package/dist/apps/chat-ui/assets/{index-BOceJ0jM.css → index-hEkrlRk-.css} +1 -1
  28. package/dist/apps/chat-ui/index.html +2 -2
  29. package/dist/apps/chat-vscode-web/assets/index-xacbCyTx.js +44 -0
  30. package/dist/apps/chat-vscode-web/index.html +1 -1
  31. package/dist/compute/pool/seeds.js +25 -3
  32. package/dist/core/events.js +4 -0
  33. package/dist/core/output-render-sequence.js +2 -0
  34. package/dist/core/provider-capacity.js +33 -0
  35. package/dist/core/provider-telemetry.js +21 -6
  36. package/dist/core/runtime-capacity.js +174 -0
  37. package/dist/core/runtime-telemetry.js +40 -12
  38. package/dist/core/session-router.js +79 -2
  39. package/dist/data/async-chat-reads.js +38 -0
  40. package/dist/data/async-chat-storage.js +91 -0
  41. package/dist/data/async-telemetry-maintenance.js +9 -0
  42. package/dist/data/bounded-worker-client.js +256 -0
  43. package/dist/data/chat-read-projections.js +159 -0
  44. package/dist/data/chat-read-worker.js +73 -0
  45. package/dist/data/chat-storage-worker.js +146 -0
  46. package/dist/data/ingest-service.js +79 -14
  47. package/dist/data/message-command-store.js +148 -0
  48. package/dist/data/payload-store.js +92 -11
  49. package/dist/data/pibo-store.js +10 -8
  50. package/dist/data/schema.js +16 -2
  51. package/dist/data/session-store.js +3 -1
  52. package/dist/data/storage-backup.js +278 -0
  53. package/dist/data/telemetry-capture.js +188 -0
  54. package/dist/data/telemetry-command.js +3 -0
  55. package/dist/data/telemetry-maintenance-worker.js +40 -0
  56. package/dist/data/telemetry-maintenance.js +110 -0
  57. package/dist/data/telemetry-retention.js +16 -7
  58. package/dist/data/telemetry-worker.js +111 -0
  59. package/dist/data/telemetry-writer.js +150 -83
  60. package/dist/data/telemetry.js +5 -0
  61. package/dist/debug/index.js +52 -0
  62. package/dist/debug/storage-backup.js +33 -0
  63. package/dist/debug/telemetry-capture.js +66 -0
  64. package/dist/gateway/server.js +2 -0
  65. package/dist/providers/openai-gpt56.js +11 -6
  66. package/dist/session-ui/terminalRows.js +48 -6
  67. package/dist/sessions/pibo-data-store.js +1 -0
  68. package/dist/shared/debug-features.js +4 -0
  69. package/dist/shared/model-inference-metrics.js +23 -0
  70. package/dist/shared/trace-event-projection.js +59 -2
  71. package/dist/shared/trace-history.js +9 -0
  72. package/dist/shared/trace-live-reducer.js +1 -0
  73. package/dist/shared/trace-patch-nodes.js +19 -0
  74. package/dist/web/channel.js +3 -0
  75. package/dist/web/http.js +36 -3
  76. package/npm-shrinkwrap.json +2 -2
  77. package/package.json +1 -1
  78. package/dist/apps/chat-ui/assets/index-BOemYq-V.js +0 -228
  79. package/dist/apps/chat-vscode-web/assets/index-CMwTB8o8.js +0 -43
@@ -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-xacbCyTx.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-b18ZkEo0.css">
10
10
  </head>
11
11
  <body>
@@ -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
+ }
@@ -383,6 +383,7 @@ function validOutputPartIndex(value) {
383
383
  export function outputIdentityFingerprint(event) {
384
384
  const payload = { ...event };
385
385
  delete payload.renderSequence;
386
+ delete payload.compactionStats;
386
387
  return createHash("sha256").update(stableJson(payload)).digest("hex");
387
388
  }
388
389
  export function outputPartFingerprint(event) {
@@ -393,6 +394,7 @@ export function outputPartFingerprint(event) {
393
394
  delete payload.usageIndex;
394
395
  delete payload.compactionIndex;
395
396
  delete payload.contentIndex;
397
+ delete payload.compactionStats;
396
398
  return createHash("sha256").update(stableJson(payload)).digest("hex");
397
399
  }
398
400
  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
  };
@@ -1,3 +1,6 @@
1
+ import { previouslyClearedMessages } from "./events.js";
2
+ import { createProviderCapacityExtension } from "./provider-capacity.js";
3
+ import { RuntimeCapacity } from "./runtime-capacity.js";
1
4
  import { randomUUID } from "node:crypto";
2
5
  import { isDeepStrictEqual } from "node:util";
3
6
  import { InitialSessionContext, } from "./profiles.js";
@@ -333,6 +336,10 @@ function providerEventTelemetryModeFromEnv(env = process.env) {
333
336
  export class PiboSessionRouter {
334
337
  options;
335
338
  sessions = new Map();
339
+ capacity;
340
+ creatingRuntimes = 0;
341
+ initializationTimings = [];
342
+ pendingStartAborts = new Map();
336
343
  pendingSessions = new Map();
337
344
  listeners = new Set();
338
345
  outputRenderSequencer;
@@ -378,6 +385,7 @@ export class PiboSessionRouter {
378
385
  closing = false;
379
386
  constructor(options = {}) {
380
387
  this.options = options;
388
+ this.capacity = new RuntimeCapacity(options.runtimeCapacity);
381
389
  this.pluginRegistry = options.pluginRegistry ?? createDefaultPiboPluginRegistry();
382
390
  // Historical custom registries supplied only actions/profiles while runtime creation was implicit.
383
391
  // Preserve that composition contract during the adapter migration without branching on adapter ids.
@@ -450,6 +458,16 @@ export class PiboSessionRouter {
450
458
  async emit(event) {
451
459
  if (this.closing)
452
460
  throw new Error("Pibo session router is disposed.");
461
+ if (event.type === "execution" && !this.sessions.has(event.piboSessionId) && (event.action === "abort" || event.action === "clear_queue")) {
462
+ this.resolvePiboSession(event.piboSessionId);
463
+ if (event.action === "abort") {
464
+ this.invalidateRunReminders([event.piboSessionId]);
465
+ this.pendingStartAborts.get(event.piboSessionId)?.abort(Object.assign(new Error("Runtime start aborted before message dispatch."), { code: "runtime_start_cancelled" }));
466
+ }
467
+ const output = { type: "execution_result", piboSessionId: event.piboSessionId, eventId: event.id, action: event.action, result: event.action === "abort" ? { aborted: true } : { cleared: previouslyClearedMessages(event) } };
468
+ this.emitOutput(output);
469
+ return output;
470
+ }
453
471
  let messageSignalAccepted = false;
454
472
  const acceptMessageSignal = () => {
455
473
  if (event.type !== "message" || !event.id || messageSignalAccepted)
@@ -1126,6 +1144,7 @@ export class PiboSessionRouter {
1126
1144
  if (this.disposePromise)
1127
1145
  return this.disposePromise;
1128
1146
  this.closing = true;
1147
+ this.capacity.close();
1129
1148
  this.disposePromise = this.disposeAllUnsafe();
1130
1149
  return this.disposePromise;
1131
1150
  }
@@ -1245,16 +1264,65 @@ export class PiboSessionRouter {
1245
1264
  const pending = this.pendingSessions.get(piboSessionId);
1246
1265
  if (pending)
1247
1266
  return pending;
1248
- const created = this.createRoutedSession(piboSessionId);
1267
+ const startAbort = new AbortController();
1268
+ this.pendingStartAborts.set(piboSessionId, startAbort);
1269
+ const created = this.createRoutedSessionWithinCapacity(piboSessionId, startAbort.signal);
1249
1270
  this.pendingSessions.set(piboSessionId, created);
1250
1271
  try {
1251
1272
  return await created;
1252
1273
  }
1253
1274
  finally {
1254
1275
  this.pendingSessions.delete(piboSessionId);
1276
+ if (this.pendingStartAborts.get(piboSessionId) === startAbort)
1277
+ this.pendingStartAborts.delete(piboSessionId);
1278
+ }
1279
+ }
1280
+ getRuntimeCapacityStatus() { return { ...this.capacity.snapshot(), telemetry: this.telemetryWriter?.status(), activeRuntimes: this.sessions.size, initializingRuntimes: this.creatingRuntimes, recentInitializations: this.initializationTimings.map(item => ({ ...item, phases: { ...item.phases } })) }; }
1281
+ async createRoutedSessionWithinCapacity(piboSessionId, signal) {
1282
+ const stored = this.resolvePiboSession(piboSessionId);
1283
+ const room = typeof stored.metadata?.chatRoomId === "string" ? stored.metadata.chatRoomId : piboSessionId;
1284
+ const queuedAt = performance.now();
1285
+ const lease = await this.capacity.starts.acquire(room, signal);
1286
+ const startedAt = performance.now();
1287
+ const phases = {};
1288
+ let outcome = "failed";
1289
+ try {
1290
+ signal.throwIfAborted();
1291
+ if (this.closing || this.quiescingSessions.has(piboSessionId))
1292
+ throw new Error("Runtime start cancelled during session shutdown.");
1293
+ if (this.sessions.size + this.creatingRuntimes >= this.capacity.maxRuntimes) {
1294
+ const idle = [...this.sessions].find(([id, session]) => { if (this.pendingSessions.has(id))
1295
+ return false; const status = session.getStatus(); return !status.disposed && !status.processing && !status.streaming && status.queuedMessages === 0; });
1296
+ if (idle)
1297
+ await this.evictIdleSession(idle[0], idle[1]);
1298
+ }
1299
+ if (this.sessions.size + this.creatingRuntimes >= this.capacity.maxRuntimes)
1300
+ throw Object.assign(new Error("Active runtime capacity reached."), { code: "runtime_capacity_unavailable" });
1301
+ this.creatingRuntimes++;
1302
+ try {
1303
+ const session = await this.createRoutedSession(piboSessionId, phases);
1304
+ if (signal.aborted) {
1305
+ if (this.sessions.get(piboSessionId) === session)
1306
+ this.sessions.delete(piboSessionId);
1307
+ await this.disposeRoutedSession(piboSessionId, session, "cold start aborted");
1308
+ throw signal.reason;
1309
+ }
1310
+ outcome = "ready";
1311
+ return session;
1312
+ }
1313
+ finally {
1314
+ this.creatingRuntimes--;
1315
+ }
1316
+ }
1317
+ finally {
1318
+ lease.release();
1319
+ this.initializationTimings.push({ sessionId: piboSessionId, waitMs: startedAt - queuedAt, totalMs: performance.now() - startedAt, phases, outcome });
1320
+ if (this.initializationTimings.length > 32)
1321
+ this.initializationTimings.shift();
1255
1322
  }
1256
1323
  }
1257
- async createRoutedSession(piboSessionId) {
1324
+ async createRoutedSession(piboSessionId, phases = {}) {
1325
+ let phaseStarted = performance.now();
1258
1326
  const piboSession = this.resolvePiboSession(piboSessionId);
1259
1327
  let session;
1260
1328
  this.signalRegistry.project({ type: "session_created", session: piboSession });
@@ -1313,6 +1381,8 @@ export class PiboSessionRouter {
1313
1381
  if (invalidProfile) {
1314
1382
  throw new Error(`Runtime profile validation failed: ${invalidProfile.message}`);
1315
1383
  }
1384
+ phases.bindingAndProfileMs = performance.now() - phaseStarted;
1385
+ phaseStarted = performance.now();
1316
1386
  let historyHandoff;
1317
1387
  if (persistedHistoryHandoff?.mode === "import") {
1318
1388
  if (!runtimeAdapter.descriptor.capabilities.historyImport) {
@@ -1336,6 +1406,8 @@ export class PiboSessionRouter {
1336
1406
  else if (persistedHistoryHandoff?.mode === "fresh") {
1337
1407
  historyHandoff = { mode: "fresh" };
1338
1408
  }
1409
+ phases.portableHistoryMs = performance.now() - phaseStarted;
1410
+ phaseStarted = performance.now();
1339
1411
  const initialFastMode = resolvePiboSessionInitialFastMode(piboSession) ?? selectRequestedFastMode(sessionProfile, modelDefaults) ?? false;
1340
1412
  const agentsController = this.createAgentsController(piboSession.id);
1341
1413
  const runToolController = this.createRunToolController(piboSession.id);
@@ -1380,6 +1452,8 @@ export class PiboSessionRouter {
1380
1452
  this.portableToolSessions.delete(piboSession.id);
1381
1453
  throw error;
1382
1454
  }
1455
+ phases.resourcesAndToolsMs = performance.now() - phaseStarted;
1456
+ phaseStarted = performance.now();
1383
1457
  const bindingSync = { expectedRevision: binding.revision };
1384
1458
  const runtimeBindingPersistence = createAgentRuntimeBindingPersistence(this.sessionStore, {
1385
1459
  piboSessionId: piboSession.id,
@@ -1423,6 +1497,7 @@ export class PiboSessionRouter {
1423
1497
  thinkingLevel: initialThinkingLevel ?? this.options.thinkingLevel,
1424
1498
  retryDefaults: resolvePiboSessionRetryDefaults(piboSession.kind, this.options.retryDefaults),
1425
1499
  extensionFactories: [
1500
+ createProviderCapacityExtension(this.capacity, piboRoomIdFromMetadata(piboSession.metadata) ?? piboSession.id, Boolean(piboSession.parentId)),
1426
1501
  ...(telemetryExtension ? [telemetryExtension] : []),
1427
1502
  ...(this.options.extensionFactories ?? []),
1428
1503
  ],
@@ -1492,6 +1567,7 @@ export class PiboSessionRouter {
1492
1567
  this.runtimeResourceSessions.delete(piboSession.id);
1493
1568
  throw error;
1494
1569
  }
1570
+ phases.adapterOpenAndBindingMs = performance.now() - phaseStarted;
1495
1571
  const resourceInspection = resources.getInspection();
1496
1572
  const statusResources = {
1497
1573
  enabledSkills: [...new Set(resourceInspection.skills.map((skill) => skill.name))],
@@ -1533,6 +1609,7 @@ export class PiboSessionRouter {
1533
1609
  this.handleInterruptedRunReminders(messages);
1534
1610
  },
1535
1611
  messagePreflight: this.options.messagePreflight,
1612
+ acquireProviderCapacity: runtimeAdapter.descriptor.id === "pi" ? undefined : (provider, signal) => this.capacity.acquireProvider(provider, typeof piboSession.metadata?.chatRoomId === "string" ? piboSession.metadata.chatRoomId : piboSession.id, signal, Boolean(piboSession.parentId)),
1536
1613
  modelFallbacks,
1537
1614
  getRuntimeAuthStatus: () => runtimeRegistry.getAgentRuntimeAuthStatus(binding.runtimeInstanceId),
1538
1615
  startRuntimeAuth: async (input) => {