@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
@@ -191,15 +191,31 @@ function createGatewayRuntimeStatuses(channelContext) {
191
191
  }
192
192
  });
193
193
  }
194
- function createGatewayStatusResponse(channelContext, options, generation) {
194
+ async function createGatewayStatusResponse(channelContext, options, generation) {
195
195
  const mode = gatewayMode(options);
196
+ const appStatuses = {};
197
+ for (const app of channelContext.getWebApps()) {
198
+ if (!app.gatewayStatus)
199
+ continue;
200
+ try {
201
+ Object.assign(appStatuses, await app.gatewayStatus());
202
+ }
203
+ catch (error) {
204
+ appStatuses[`${app.name}Status`] = { status: "ambiguous", error: error instanceof Error ? error.message : "Status unavailable" };
205
+ }
206
+ }
207
+ const durable = appStatuses.durableMessageQueue;
196
208
  return responseJson({
197
- status: "ok",
209
+ status: durable?.status === "degraded" || durable?.status === "ambiguous" ? "degraded" : "ok",
198
210
  mode,
199
211
  generation,
200
- health: { status: "ok", mode },
212
+ health: { status: durable?.status === "degraded" || durable?.status === "ambiguous" ? "degraded" : "ok", mode },
213
+ runtimeQueue: { layer: "runtime-session", statuses: createGatewayRuntimeStatuses(channelContext) },
201
214
  runtimeStatuses: createGatewayRuntimeStatuses(channelContext),
215
+ ...(channelContext.getRuntimeCapacityStatus ? { runtimeCapacity: channelContext.getRuntimeCapacityStatus() } : {}),
216
+ ...(channelContext.getRunJobReliabilityStatus ? { reliability: channelContext.getRunJobReliabilityStatus() } : {}),
202
217
  activeRuns: collectActiveRuns(channelContext),
218
+ ...appStatuses,
203
219
  });
204
220
  }
205
221
  function createCanonicalRedirect(request, canonicalBaseURL) {
@@ -217,6 +233,68 @@ function createCanonicalRedirect(request, canonicalBaseURL) {
217
233
  function isEventStreamResponse(response) {
218
234
  return response.headers.get("content-type")?.toLowerCase().startsWith("text/event-stream") === true;
219
235
  }
236
+ function responseCanStart(response) {
237
+ return !response.destroyed && !response.writableEnded && !response.writableFinished && !response.headersSent;
238
+ }
239
+ function responseState(response) {
240
+ return [
241
+ `destroyed=${response.destroyed}`,
242
+ `writableEnded=${response.writableEnded}`,
243
+ `writableFinished=${response.writableFinished}`,
244
+ `headersSent=${response.headersSent}`,
245
+ ].join(",");
246
+ }
247
+ function requestPath(request) {
248
+ return (request.url ?? "/").split(/[?#]/, 1)[0].slice(0, 256).replace(/[\r\n]/g, "_");
249
+ }
250
+ function errorIdentity(error) {
251
+ try {
252
+ if (!(error instanceof Error))
253
+ return typeof error;
254
+ const rawName = typeof error.name === "string" ? error.name : "Error";
255
+ const name = rawName.replace(/[^A-Za-z0-9_.-]/g, "").slice(0, 64) || "Error";
256
+ const code = "code" in error && typeof error.code === "string" ? error.code.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 64) : undefined;
257
+ return code ? `${name}:${code}` : name;
258
+ }
259
+ catch {
260
+ return "Error";
261
+ }
262
+ }
263
+ function logHttpBoundaryFailure(phase, request, error, response) {
264
+ try {
265
+ const method = (request.method ?? "UNKNOWN").replace(/[^A-Z]/gi, "").slice(0, 16) || "UNKNOWN";
266
+ const state = response ? ` response={${responseState(response)}}` : "";
267
+ console.error(`[web-host] contained ${phase} failure method=${method} path=${requestPath(request)} error=${errorIdentity(error)}${state}`);
268
+ }
269
+ catch {
270
+ // Diagnostics are best-effort and must never reopen the request rejection path.
271
+ }
272
+ }
273
+ function terminateResponse(response, error) {
274
+ if (response.destroyed || response.writableEnded || response.writableFinished)
275
+ return;
276
+ try {
277
+ response.destroy(error instanceof Error ? error : undefined);
278
+ }
279
+ catch {
280
+ // A broken response implementation must not escape the request boundary.
281
+ }
282
+ }
283
+ function endUpgradeSocket(socket, statusLine) {
284
+ if (socket.destroyed)
285
+ return;
286
+ try {
287
+ socket.end(statusLine);
288
+ }
289
+ catch {
290
+ try {
291
+ socket.destroy();
292
+ }
293
+ catch {
294
+ // A broken socket implementation must not escape the upgrade boundary.
295
+ }
296
+ }
297
+ }
220
298
  async function waitForServerClose(closePromise, timeoutMs) {
221
299
  return await new Promise((resolve, reject) => {
222
300
  let settled = false;
@@ -322,7 +400,7 @@ export function createWebHostChannel(options = {}) {
322
400
  return;
323
401
  }
324
402
  if (url.pathname === "/gateway/status") {
325
- await sendResponse(nodeResponse, createGatewayStatusResponse(requireContext(), options, generation));
403
+ await sendResponse(nodeResponse, await createGatewayStatusResponse(requireContext(), options, generation));
326
404
  return;
327
405
  }
328
406
  if (url.pathname.startsWith("/api/auth/")) {
@@ -359,8 +437,18 @@ export function createWebHostChannel(options = {}) {
359
437
  }
360
438
  catch (error) {
361
439
  const status = error instanceof PiboAuthError || error instanceof PiboWebHttpError ? error.statusCode : 500;
362
- if (!nodeResponse.destroyed) {
363
- await sendResponse(nodeResponse, responseJson({ error: error instanceof Error ? error.message : String(error) }, { status }));
440
+ logHttpBoundaryFailure("request", nodeRequest, error, nodeResponse);
441
+ if (responseCanStart(nodeResponse)) {
442
+ try {
443
+ await sendResponse(nodeResponse, responseJson({ error: error instanceof Error ? error.message : String(error) }, { status }));
444
+ }
445
+ catch (responseError) {
446
+ logHttpBoundaryFailure("error-response", nodeRequest, responseError, nodeResponse);
447
+ terminateResponse(nodeResponse, responseError);
448
+ }
449
+ }
450
+ else {
451
+ terminateResponse(nodeResponse, error);
364
452
  }
365
453
  }
366
454
  finally {
@@ -383,10 +471,9 @@ export function createWebHostChannel(options = {}) {
383
471
  await app.handleUpgrade(nodeRequest, socket, head, createAppContext(ctx), requestURL);
384
472
  }
385
473
  catch (error) {
386
- if (!socket.destroyed) {
387
- const unauthorized = error instanceof PiboAuthError || error instanceof PiboWebHttpError;
388
- socket.end(`HTTP/1.1 ${unauthorized ? 401 : 502} ${unauthorized ? "Unauthorized" : "Bad Gateway"}\r\nConnection: close\r\n\r\n`);
389
- }
474
+ logHttpBoundaryFailure("upgrade", nodeRequest, error);
475
+ const unauthorized = error instanceof PiboAuthError || error instanceof PiboWebHttpError;
476
+ endUpgradeSocket(socket, `HTTP/1.1 ${unauthorized ? 401 : 502} ${unauthorized ? "Unauthorized" : "Bad Gateway"}\r\nConnection: close\r\n\r\n`);
390
477
  }
391
478
  };
392
479
  return {
@@ -401,11 +488,29 @@ export function createWebHostChannel(options = {}) {
401
488
  return;
402
489
  shuttingDown = false;
403
490
  context = channelContext;
491
+ for (const app of channelContext.getWebApps())
492
+ await app.initialize?.(createAppContext(channelContext));
404
493
  server = createServer((request, response) => {
405
- void handleRequest(request, response);
494
+ void handleRequest(request, response).catch((error) => {
495
+ try {
496
+ logHttpBoundaryFailure("request-terminal", request, error, response);
497
+ terminateResponse(response, error);
498
+ }
499
+ catch {
500
+ // The terminal request boundary itself is intentionally nonthrowing.
501
+ }
502
+ });
406
503
  });
407
504
  server.on("upgrade", (request, socket, head) => {
408
- void handleUpgrade(request, socket, head);
505
+ void handleUpgrade(request, socket, head).catch((error) => {
506
+ try {
507
+ logHttpBoundaryFailure("upgrade-terminal", request, error);
508
+ endUpgradeSocket(socket);
509
+ }
510
+ catch {
511
+ // The terminal upgrade boundary itself is intentionally nonthrowing.
512
+ }
513
+ });
409
514
  });
410
515
  server.on("connection", (socket) => {
411
516
  sockets.add(socket);
package/dist/web/http.js CHANGED
@@ -78,61 +78,136 @@ export async function nodeRequestToWebRequest(request, baseURL, signal) {
78
78
  });
79
79
  }
80
80
  export async function sendWebResponse(response, webResponse, options = {}) {
81
+ assertResponseCanStart(response);
81
82
  const headers = responseHeaders(webResponse);
82
83
  const compressEncoding = preferredResponseEncoding(response.req?.headers["accept-encoding"], webResponse);
83
- if (compressEncoding && webResponse.body) {
84
- const body = await readResponseBody(webResponse);
85
- if (body.length >= MIN_COMPRESS_RESPONSE_BYTES && body.length <= MAX_SYNC_GZIP_RESPONSE_BYTES) {
86
- const compressionStartedAt = performance.now();
87
- const compressed = gzipSync(body, { level: 1 });
88
- appendServerTimingHeader(headers, `response_compress;dur=${(performance.now() - compressionStartedAt).toFixed(1)}`);
89
- headers["content-encoding"] = compressEncoding;
90
- headers["content-length"] = String(compressed.length);
91
- headers.vary = appendVary(headers.vary, "accept-encoding");
84
+ let reader = webResponse.body?.getReader();
85
+ let wroteHeaders = false;
86
+ let clientClosed = false;
87
+ const cancelReader = () => {
88
+ if (reader)
89
+ void reader.cancel().catch(() => undefined);
90
+ };
91
+ const close = () => {
92
+ clientClosed = true;
93
+ cancelReader();
94
+ };
95
+ const abort = () => {
96
+ cancelReader();
97
+ try {
98
+ if (!response.destroyed && !response.writableEnded)
99
+ response.end();
100
+ }
101
+ catch {
102
+ tryDestroyResponse(response);
103
+ }
104
+ try {
105
+ response.socket?.end();
106
+ }
107
+ catch {
108
+ tryDestroyResponse(response);
109
+ }
110
+ };
111
+ if (reader) {
112
+ response.once("close", close);
113
+ options.signal?.addEventListener("abort", abort, { once: true });
114
+ if (options.signal?.aborted)
115
+ abort();
116
+ }
117
+ try {
118
+ if (compressEncoding && reader) {
119
+ const body = await readResponseBody(reader);
120
+ if (clientClosed || options.signal?.aborted || response.destroyed || response.writableEnded || response.writableFinished)
121
+ return;
122
+ assertResponseCanStart(response);
123
+ if (body.length >= MIN_COMPRESS_RESPONSE_BYTES && body.length <= MAX_SYNC_GZIP_RESPONSE_BYTES) {
124
+ const compressionStartedAt = performance.now();
125
+ const compressed = gzipSync(body, { level: 1 });
126
+ appendServerTimingHeader(headers, `response_compress;dur=${(performance.now() - compressionStartedAt).toFixed(1)}`);
127
+ headers["content-encoding"] = compressEncoding;
128
+ headers["content-length"] = String(compressed.length);
129
+ headers.vary = appendVary(headers.vary, "accept-encoding");
130
+ response.writeHead(webResponse.status, headers);
131
+ wroteHeaders = true;
132
+ response.end(compressed);
133
+ return;
134
+ }
135
+ if (body.length > MAX_SYNC_GZIP_RESPONSE_BYTES) {
136
+ headers["x-pibo-compression-skipped"] = "sync-gzip-size-limit";
137
+ }
92
138
  response.writeHead(webResponse.status, headers);
93
- response.end(compressed);
139
+ wroteHeaders = true;
140
+ response.end(body);
94
141
  return;
95
142
  }
96
- if (body.length > MAX_SYNC_GZIP_RESPONSE_BYTES) {
97
- headers["x-pibo-compression-skipped"] = "sync-gzip-size-limit";
98
- }
99
143
  response.writeHead(webResponse.status, headers);
100
- response.end(body);
101
- return;
102
- }
103
- response.writeHead(webResponse.status, headers);
104
- if (!webResponse.body) {
105
- response.end();
106
- return;
107
- }
108
- const reader = webResponse.body.getReader();
109
- const cancel = () => {
110
- void reader.cancel();
111
- };
112
- const abort = () => {
113
- const socket = response.socket;
114
- void reader.cancel();
115
- if (!response.writableEnded)
144
+ wroteHeaders = true;
145
+ if (!reader) {
116
146
  response.end();
117
- socket?.end();
118
- };
119
- response.once("close", cancel);
120
- options.signal?.addEventListener("abort", abort, { once: true });
121
- if (options.signal?.aborted)
122
- abort();
123
- try {
147
+ return;
148
+ }
124
149
  while (true) {
125
150
  const { done, value } = await reader.read();
126
151
  if (done)
127
152
  break;
128
- response.write(Buffer.from(value));
153
+ const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
154
+ for (let offset = 0; offset < bytes.length; offset += 64 * 1024) {
155
+ if (response.destroyed || response.writableEnded || response.writableFinished) {
156
+ cancelReader();
157
+ return;
158
+ }
159
+ if (!response.write(bytes.subarray(offset, offset + 64 * 1024))) {
160
+ const drained = await waitForResponseDrain(response, (webResponse.headers.get("content-type") ?? "").startsWith("text/event-stream") ? 5000 : undefined, options.signal);
161
+ if (!drained) {
162
+ cancelReader();
163
+ tryDestroyResponse(response);
164
+ return;
165
+ }
166
+ }
167
+ }
129
168
  }
130
- if (!response.writableEnded)
169
+ if (!response.destroyed && !response.writableEnded && !response.writableFinished)
131
170
  response.end();
132
171
  }
172
+ catch (error) {
173
+ cancelReader();
174
+ if (clientClosed || response.destroyed || response.writableEnded || response.writableFinished)
175
+ return;
176
+ if (wroteHeaders || response.headersSent)
177
+ tryDestroyResponse(response, error);
178
+ throw error;
179
+ }
133
180
  finally {
134
- response.off("close", cancel);
181
+ response.off("close", close);
135
182
  options.signal?.removeEventListener("abort", abort);
183
+ if (reader) {
184
+ try {
185
+ reader.releaseLock();
186
+ }
187
+ catch {
188
+ // Cancellation/read settlement owns the lock until the pending read resolves.
189
+ }
190
+ }
191
+ }
192
+ }
193
+ function assertResponseCanStart(response) {
194
+ if (response.destroyed)
195
+ throw new Error("HTTP response is destroyed");
196
+ if (response.writableEnded)
197
+ throw new Error("HTTP response has ended");
198
+ if (response.writableFinished)
199
+ throw new Error("HTTP response has finished");
200
+ if (response.headersSent)
201
+ throw new Error("HTTP response headers were already sent");
202
+ }
203
+ function tryDestroyResponse(response, error) {
204
+ if (response.destroyed)
205
+ return;
206
+ try {
207
+ response.destroy(error instanceof Error ? error : undefined);
208
+ }
209
+ catch {
210
+ // The request boundary is best-effort once the response implementation itself fails.
136
211
  }
137
212
  }
138
213
  function appendServerTimingHeader(headers, value) {
@@ -185,9 +260,8 @@ function responseCanBeCompressed(webResponse) {
185
260
  const contentType = webResponse.headers.get("content-type") ?? "";
186
261
  return /^application\/json\b/.test(contentType);
187
262
  }
188
- async function readResponseBody(webResponse) {
263
+ async function readResponseBody(reader) {
189
264
  const chunks = [];
190
- const reader = webResponse.body.getReader();
191
265
  while (true) {
192
266
  const { done, value } = await reader.read();
193
267
  if (done)
@@ -206,3 +280,23 @@ function appendVary(existing, value) {
206
280
  function appendServerTiming(existing, value) {
207
281
  return existing ? `${existing}, ${value}` : value;
208
282
  }
283
+ /** Honor socket pressure before pulling another application frame. */
284
+ function waitForResponseDrain(response, maximumAgeMs, signal) {
285
+ if (response.destroyed || response.writableEnded || signal?.aborted)
286
+ return Promise.resolve(false);
287
+ return new Promise(resolve => {
288
+ let timer;
289
+ const finish = (ready) => { if (timer)
290
+ clearTimeout(timer); response.off("drain", drain); response.off("close", close); response.off("error", close); signal?.removeEventListener("abort", close); resolve(ready); };
291
+ const drain = () => finish(true);
292
+ const close = () => finish(false);
293
+ response.once("drain", drain);
294
+ response.once("close", close);
295
+ response.once("error", close);
296
+ signal?.addEventListener("abort", close, { once: true });
297
+ if (maximumAgeMs !== undefined) {
298
+ timer = setTimeout(close, maximumAgeMs);
299
+ timer.unref();
300
+ }
301
+ });
302
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.4.4",
3
+ "version": "3.5.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "3.4.4",
9
+ "version": "3.5.1",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.4.4",
3
+ "version": "3.5.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",