@pasko70/pibo 3.4.3 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/agent-runtime/routed-session.js +39 -12
  2. package/dist/agent-runtimes/codex-native/adapter.js +14 -1
  3. package/dist/agent-runtimes/codex-native/models.js +4 -4
  4. package/dist/agent-runtimes/codex-native/process.js +32 -13
  5. package/dist/agent-runtimes/codex-native/provider-usage.js +115 -0
  6. package/dist/agent-runtimes/pi/adapter.js +1 -0
  7. package/dist/agent-runtimes/pi/routed-session.js +4 -2
  8. package/dist/apps/chat/bounded-event-stream.js +98 -0
  9. package/dist/apps/chat/chat-settings-routes.js +3 -3
  10. package/dist/apps/chat/data/chat-data-mappers.js +32 -13
  11. package/dist/apps/chat/data/event-command-service.js +30 -21
  12. package/dist/apps/chat/data/history-query-service.js +34 -25
  13. package/dist/apps/chat/data/read-state-service.js +13 -0
  14. package/dist/apps/chat/data/session-query-service.js +19 -11
  15. package/dist/apps/chat/data/timeline-query-service.js +19 -7
  16. package/dist/apps/chat/message-command-dispatcher.js +134 -0
  17. package/dist/apps/chat/output-compactor.js +9 -0
  18. package/dist/apps/chat/output-event-policy.js +9 -1
  19. package/dist/apps/chat/stream.js +21 -3
  20. package/dist/apps/chat/telemetry-retention-service.js +113 -8
  21. package/dist/apps/chat/trace-response-cache.js +46 -0
  22. package/dist/apps/chat/trace-v2.js +12 -6
  23. package/dist/apps/chat/trace.js +1 -1
  24. package/dist/apps/chat/web-app.js +608 -281
  25. package/dist/apps/chat-ui/assets/{dist-DeMKnZR8.js → dist-BG0n7zLd.js} +1 -1
  26. package/dist/apps/chat-ui/assets/{dist-C8GzUMPk.js → dist-D6TjFhAm.js} +1 -1
  27. package/dist/apps/chat-ui/assets/{dist-BkUu8WPA.js → dist-D79vyxSX.js} +1 -1
  28. package/dist/apps/chat-ui/assets/{dist-B4YhOxh0.js → dist-DFZ8cwh0.js} +1 -1
  29. package/dist/apps/chat-ui/assets/{dist-Cyb4rVa6.js → dist-cOjokPrK.js} +1 -1
  30. package/dist/apps/chat-ui/assets/index-RMHUTJ62.js +229 -0
  31. package/dist/apps/chat-ui/assets/{index-BOceJ0jM.css → index-hEkrlRk-.css} +1 -1
  32. package/dist/apps/chat-ui/index.html +2 -2
  33. package/dist/apps/chat-vscode-web/assets/index-xacbCyTx.js +44 -0
  34. package/dist/apps/chat-vscode-web/index.html +1 -1
  35. package/dist/compute/pool/seeds.js +25 -3
  36. package/dist/core/events.js +4 -0
  37. package/dist/core/output-render-sequence.js +2 -0
  38. package/dist/core/provider-capacity.js +33 -0
  39. package/dist/core/provider-telemetry.js +21 -6
  40. package/dist/core/runtime-capacity.js +174 -0
  41. package/dist/core/runtime-telemetry.js +40 -12
  42. package/dist/core/session-router.js +79 -2
  43. package/dist/data/async-chat-reads.js +38 -0
  44. package/dist/data/async-chat-storage.js +91 -0
  45. package/dist/data/async-telemetry-maintenance.js +9 -0
  46. package/dist/data/bounded-worker-client.js +256 -0
  47. package/dist/data/chat-read-projections.js +159 -0
  48. package/dist/data/chat-read-worker.js +73 -0
  49. package/dist/data/chat-storage-worker.js +146 -0
  50. package/dist/data/ingest-service.js +79 -14
  51. package/dist/data/message-command-store.js +148 -0
  52. package/dist/data/payload-store.js +92 -11
  53. package/dist/data/pibo-store.js +10 -8
  54. package/dist/data/schema.js +16 -2
  55. package/dist/data/session-store.js +3 -1
  56. package/dist/data/storage-backup.js +278 -0
  57. package/dist/data/telemetry-capture.js +188 -0
  58. package/dist/data/telemetry-command.js +3 -0
  59. package/dist/data/telemetry-maintenance-worker.js +40 -0
  60. package/dist/data/telemetry-maintenance.js +110 -0
  61. package/dist/data/telemetry-retention.js +16 -7
  62. package/dist/data/telemetry-worker.js +111 -0
  63. package/dist/data/telemetry-writer.js +150 -83
  64. package/dist/data/telemetry.js +5 -0
  65. package/dist/debug/index.js +52 -0
  66. package/dist/debug/storage-backup.js +33 -0
  67. package/dist/debug/telemetry-capture.js +66 -0
  68. package/dist/gateway/cli.js +104 -16
  69. package/dist/gateway/server.js +2 -0
  70. package/dist/providers/openai-gpt56.js +11 -6
  71. package/dist/session-ui/terminalRows.js +55 -13
  72. package/dist/sessions/pibo-data-store.js +30 -10
  73. package/dist/shared/debug-features.js +4 -0
  74. package/dist/shared/model-inference-metrics.js +23 -0
  75. package/dist/shared/trace-event-projection.js +59 -2
  76. package/dist/shared/trace-history.js +9 -0
  77. package/dist/shared/trace-live-reducer.js +1 -0
  78. package/dist/shared/trace-patch-nodes.js +19 -0
  79. package/dist/web/channel.js +8 -2
  80. package/dist/web/http.js +36 -3
  81. package/npm-shrinkwrap.json +2 -2
  82. package/package.json +1 -1
  83. package/dist/apps/chat-ui/assets/index-Dk4mbXAB.js +0 -228
  84. package/dist/apps/chat-vscode-web/assets/index-0oTGFHni.js +0 -43
@@ -1,3 +1,11 @@
1
+ import { Readable } from "node:stream";
2
+ import { BoundedEventStream } from "./bounded-event-stream.js";
3
+ import { boundedMessageBytes } from "../../data/bounded-worker-client.js";
4
+ import { TraceResponseCache } from "./trace-response-cache.js";
5
+ import { AsyncChatReadQueries } from "../../data/async-chat-reads.js";
6
+ import { MessageCommandDispatcher } from "./message-command-dispatcher.js";
7
+ import { piboHomePath } from "../../core/pibo-home.js";
8
+ import { AsyncChatStorage } from "../../data/async-chat-storage.js";
1
9
  import { createHash, randomUUID } from "node:crypto";
2
10
  import os from "node:os";
3
11
  import { dirname, join } from "node:path";
@@ -25,7 +33,7 @@ import { loadPiboModelDefaults, } from "../../core/model-defaults.js";
25
33
  import { inspectPiboContextBuild } from "../../core/context-build.js";
26
34
  import { isPiboThinkingLevel } from "../../core/thinking.js";
27
35
  import { loadPiboUserSettings, updateTelemetryRetentionLastPrunedAt } from "../../core/user-settings.js";
28
- import { isTelemetryRetentionMaintenanceDue, maybeRunTelemetryRetentionMaintenance } from "./telemetry-retention-service.js";
36
+ import { disposeTelemetryRetentionMaintenance, isTelemetryRetentionMaintenanceDue, maybeRunTelemetryRetentionMaintenance } from "./telemetry-retention-service.js";
29
37
  import { loadModelCatalog } from "./model-catalog.js";
30
38
  import { createCustomAgentProfileDefinition, createCustomAgentRuntimeValidationProfile } from "./agent-profiles.js";
31
39
  import { createDefaultPiboReliabilityStore, PiboReliabilityStore } from "../../reliability/store.js";
@@ -114,20 +122,28 @@ const RELIABILITY_INLINE_PAYLOAD_MAX_BYTES = 64 * 1024;
114
122
  const RESOURCE_WARNING_RING_MAX = 25;
115
123
  function writeSse(controller, event, payload, id) {
116
124
  const encoder = new TextEncoder();
117
- if (id)
118
- controller.enqueue(encoder.encode(`id: ${id}\n`));
119
- controller.enqueue(encoder.encode(`event: ${event}\n`));
120
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`));
125
+ try {
126
+ boundedMessageBytes(payload, 2 * 1024 * 1024 - 1024);
127
+ }
128
+ catch {
129
+ controller.error(Error("Oversized SSE frame requires cursor replay"));
130
+ return;
131
+ }
132
+ controller.enqueue(encoder.encode(`${id ? `id: ${id}\n` : ""}event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`));
121
133
  }
122
134
  function writeSseComment(controller, comment) {
123
135
  controller.enqueue(new TextEncoder().encode(`: ${comment}\n\n`));
124
136
  }
125
137
  function writeJsonSse(controller, event, payload, id) {
126
138
  const encoder = new TextEncoder();
127
- if (id)
128
- controller.enqueue(encoder.encode(`id: ${id}\n`));
129
- controller.enqueue(encoder.encode(`event: ${event}\n`));
130
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`));
139
+ try {
140
+ boundedMessageBytes(payload, 2 * 1024 * 1024 - 1024);
141
+ }
142
+ catch {
143
+ controller.error(Error("Oversized SSE frame requires cursor replay"));
144
+ return;
145
+ }
146
+ controller.enqueue(encoder.encode(`${id ? `id: ${id}\n` : ""}event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`));
131
147
  }
132
148
  function compactSignalStatusPatch(patch) {
133
149
  return {
@@ -321,7 +337,12 @@ function serializeGatewayResourceDiagnostics(state) {
321
337
  maxMs: Number.isFinite(state.eventLoopDelay.max) ? state.eventLoopDelay.max / 1_000_000 : 0,
322
338
  p95Ms: state.eventLoopDelay.percentile(95) / 1_000_000,
323
339
  },
340
+ readWorker: state.readQueries?.status(),
324
341
  streams: {
342
+ boundedConnections: state.boundedStreams.size,
343
+ queuedBytes: [...state.boundedStreams].reduce((sum, stream) => sum + stream.status().bytes, 0),
344
+ oldestQueuedAgeMs: Math.max(0, ...[...state.boundedStreams].map(stream => stream.status().oldestAgeMs)),
345
+ budgetDisconnects: state.boundedStreamDisconnects,
325
346
  liveListeners: state.liveListeners.size,
326
347
  activeEventStreams,
327
348
  activeTraceSessions: state.activeTraceSessions.size,
@@ -391,6 +412,8 @@ function createFastTraceV2Version(input) {
391
412
  },
392
413
  productHistory: {
393
414
  messageCount: input.productHistory?.messageCount ?? 0,
415
+ revision: input.productHistory?.revision ?? 0,
416
+ complete: input.productHistory?.complete ?? true,
394
417
  firstEventSequence: input.productHistory?.firstEventSequence ?? null,
395
418
  lastEventSequence: input.productHistory?.lastEventSequence ?? null,
396
419
  lastCreatedAt: input.productHistory?.lastCreatedAt ?? null,
@@ -401,7 +424,7 @@ function createFastTraceV2Version(input) {
401
424
  .digest("hex");
402
425
  }
403
426
  function ensureEventIndexing(state, context) {
404
- if (state.subscribedContext === context && state.unsubscribe)
427
+ if (state.subscribedContext?.channelContext === context.channelContext && state.unsubscribe)
405
428
  return;
406
429
  state.unsubscribe?.();
407
430
  state.subscribedContext = context;
@@ -487,7 +510,7 @@ function createWebOutputPersistenceJob(input) {
487
510
  onDeadLetter: input.onDeadLetter,
488
511
  };
489
512
  }
490
- function deliverWebOutputPersistenceState(state, context, retryContext) {
513
+ async function deliverWebOutputPersistenceState(state, context, retryContext) {
491
514
  const persistenceState = parseWebOutputPersistenceState(retryContext.payload);
492
515
  if (!persistenceState)
493
516
  throw new Error("Invalid durable web output persistence state");
@@ -507,29 +530,32 @@ function deliverWebOutputPersistenceState(state, context, retryContext) {
507
530
  try {
508
531
  if (!delivery.v2) {
509
532
  const createdAt = new Date().toISOString();
510
- const ingested = state.ingestService.ingestOutputEvent({
533
+ const ingestInput = {
511
534
  session,
512
535
  roomId: persistenceState.roomId,
513
536
  actorId: persistenceState.actorId ?? session.id,
514
537
  event: delivery.event,
515
538
  createdAt,
516
- });
517
- const storedEvent = state.dataStore.eventLog.findByIdempotencyKey(delivery.deliveryId);
518
- if (!storedEvent || storedEvent.streamId !== ingested.streamId) {
539
+ };
540
+ const asyncIngested = state.asyncStorage ? await state.asyncStorage.ingestOutput(ingestInput) : undefined;
541
+ const ingested = asyncIngested ?? state.ingestService.ingestOutputEvent(ingestInput);
542
+ const storedEvent = asyncIngested?.stored ?? state.dataStore.eventLog.findByIdempotencyKey(delivery.deliveryId);
543
+ if (!storedEvent || (!asyncIngested && "streamId" in storedEvent && storedEvent.streamId !== ingested.streamId)) {
519
544
  throw new Error(`Missing V2 event ${ingested.streamId} for ${delivery.deliveryId}`);
520
545
  }
521
546
  delivery.v2 = {
522
547
  streamId: ingested.streamId,
523
548
  createdAt: storedEvent.createdAt,
524
- eventId: eventIdentityForDelivery(delivery.event),
549
+ eventId: asyncIngested?.stored.eventId ?? eventIdentityForDelivery(delivery.event),
525
550
  duplicate: ingested.duplicate,
526
551
  };
527
552
  checkpoint();
528
553
  }
554
+ state.commandDispatcher?.outputPersisted(delivery.event);
529
555
  if (!delivery.reliabilityDelivered) {
530
556
  if (delivery.reliabilityPayload === undefined) {
531
557
  delivery.reliabilityPayload = boundedReliabilityOutputPayload(state, delivery.event);
532
- checkpoint();
558
+ // Preparation is replayable; checkpoint together with the confirmed append below.
533
559
  }
534
560
  const deliveryKey = delivery.deliveryId;
535
561
  state.reliabilityStore.appendOnce({
@@ -545,10 +571,19 @@ function deliverWebOutputPersistenceState(state, context, retryContext) {
545
571
  }
546
572
  if (!delivery.sideEffectsDelivered && state.reliabilityStore.hasDeliveryReceipt(delivery.deliveryId, "chat-web-observable-v1")) {
547
573
  delivery.sideEffectsDelivered = true;
548
- checkpoint();
574
+ // The durable receipt is authoritative; the job can now finish without another payload rewrite.
549
575
  }
550
576
  else if (!delivery.sideEffectsDelivered) {
551
- const stored = storedChatEventForDelivery(persistenceState, delivery);
577
+ let stored = storedChatEventForDelivery(persistenceState, delivery);
578
+ try {
579
+ boundedMessageBytes(stored.payload, 64 * 1024);
580
+ }
581
+ catch {
582
+ const [persisted] = await (state.readQueries?.timeline ?? state.timelineQuery).listEvents({ piboSessionId: stored.piboSessionId, afterStreamId: stored.streamId - 1, limit: 1 });
583
+ if (!persisted || persisted.streamId !== stored.streamId)
584
+ throw new Error("Persisted output unavailable for live delivery");
585
+ stored = persisted;
586
+ }
552
587
  if (delivery.event.type === "assistant_message" || delivery.event.type === "message_finished" || delivery.event.type === "session_error") {
553
588
  markActiveSessionRead(state, delivery.event.piboSessionId, stored.streamId);
554
589
  }
@@ -566,7 +601,7 @@ function deliverWebOutputPersistenceState(state, context, retryContext) {
566
601
  // recording before sends would trade duplicates for silent loss.
567
602
  state.reliabilityStore.recordDeliveryReceipt(delivery.deliveryId, "chat-web-observable-v1");
568
603
  delivery.sideEffectsDelivered = true;
569
- checkpoint();
604
+ // The durable receipt is authoritative; the job can now finish without another payload rewrite.
570
605
  }
571
606
  }
572
607
  catch (error) {
@@ -754,12 +789,45 @@ function markActiveSessionRead(state, piboSessionId, streamId) {
754
789
  return;
755
790
  state.readState.markSessionRead(piboSessionId, streamId);
756
791
  }
792
+ async function readNavigationIndex(state, roomId) {
793
+ if (!state.readQueries)
794
+ return state.sessionQuery.listSessions(roomId);
795
+ const items = [];
796
+ let afterId;
797
+ for (;;) {
798
+ const page = await state.readQueries.navigation.sessionIndexPage({ roomId, afterId, limit: 500 });
799
+ items.push(...page);
800
+ if (page.length < 500)
801
+ break;
802
+ afterId = page.at(-1).piboSessionId;
803
+ }
804
+ return items;
805
+ }
806
+ const sharedSessionSnapshots = new Map();
757
807
  function listSharedSessions(context) {
758
- const sessions = context.channelContext.listSessions?.() ?? context.channelContext.findSessions({});
808
+ const revision = context.channelContext.getSessionStructureRevision?.();
759
809
  const profiles = context.channelContext.getProfiles?.();
760
- return sessions
761
- .map((session) => canonicalizeSessionProfile(context, session, profiles))
762
- .sort(compareChatWebSessionsBySidebarOrder);
810
+ const profileKey = JSON.stringify(profiles?.map(profile => [profile.name, profile.aliases]) ?? []);
811
+ const cached = sharedSessionSnapshots.get(context.channelContext);
812
+ if (revision !== undefined && cached?.revision === revision && cached.profiles === profileKey)
813
+ return cached.sessions.slice();
814
+ const sessions = (context.channelContext.listSessions?.() ?? context.channelContext.findSessions({})).map(session => canonicalizeSessionProfile(context, session, profiles)).sort(compareChatWebSessionsBySidebarOrder);
815
+ if (revision !== undefined) {
816
+ sharedSessionSnapshots.delete(context.channelContext);
817
+ try {
818
+ let bytes = 16;
819
+ for (const session of sessions) {
820
+ bytes += boundedMessageBytes(session, 16 * 1024 * 1024 - bytes);
821
+ if (bytes > 16 * 1024 * 1024)
822
+ throw Error("Session snapshot budget exceeded");
823
+ }
824
+ sharedSessionSnapshots.set(context.channelContext, { revision: context.channelContext.getSessionStructureRevision?.() ?? revision, profiles: profileKey, sessions, bytes });
825
+ while (sharedSessionSnapshots.size > 4 || [...sharedSessionSnapshots.values()].reduce((total, item) => total + item.bytes, 0) > 16 * 1024 * 1024)
826
+ sharedSessionSnapshots.delete(sharedSessionSnapshots.keys().next().value);
827
+ }
828
+ catch { }
829
+ }
830
+ return sessions.slice();
763
831
  }
764
832
  function canonicalizeSessionProfile(context, session, profiles = context.channelContext.getProfiles?.()) {
765
833
  const canonicalProfile = canonicalProfileName(profiles, session.profile);
@@ -2798,11 +2866,13 @@ function sessionSubtree(sessions, rootSessionId) {
2798
2866
  }
2799
2867
  return [...subtree.values()];
2800
2868
  }
2801
- function buildSessionUnreadCounts(state, sessions) {
2869
+ async function buildSessionUnreadCounts(state, sessions) {
2802
2870
  const sessionsById = new Map(sessions.map((session) => [session.id, session]));
2803
2871
  const visibleSessionIds = sessions
2804
2872
  .filter((session) => !hasArchivedSessionInPath(session, sessionsById))
2805
2873
  .map((session) => session.id);
2874
+ if (state.readQueries)
2875
+ return new Map(await state.readQueries.navigation.unreadCountsPage({ piboSessionIds: visibleSessionIds }));
2806
2876
  return state.readState.countUnreadMessagesBySession({
2807
2877
  piboSessionIds: visibleSessionIds,
2808
2878
  });
@@ -3141,10 +3211,30 @@ function writeChatEventFrames(controller, event, state, cursor, options = { mode
3141
3211
  return;
3142
3212
  if (options.mode === "summary" && isLiveOnlyOutputEvent(event.payload))
3143
3213
  return;
3214
+ if (isLiveOnlyOutputEvent(event.payload)) {
3215
+ try {
3216
+ boundedMessageBytes(event.payload, 1024 * 1024);
3217
+ }
3218
+ catch {
3219
+ return;
3220
+ }
3221
+ }
3144
3222
  const piboSessionId = event.piboSessionId ?? event.payload.piboSessionId;
3145
3223
  const streamId = "streamId" in event ? event.streamId : undefined;
3146
3224
  const createdAt = chatLiveEventCreatedAt(event);
3147
- const frames = chatStreamFramesFromOutputEvent(event.payload, state, {
3225
+ const ref = "storedPayloadRef" in event ? event.storedPayloadRef : undefined;
3226
+ let payload = event.payload;
3227
+ if (ref && ref.byteLength > 64 * 1024) {
3228
+ if (payload.type === "assistant_message" || payload.type === "thinking_finished")
3229
+ payload = { ...payload, text: ref.preview };
3230
+ else if (payload.type === "tool_execution_finished")
3231
+ payload = { ...payload, result: null };
3232
+ else if (payload.type === "tool_execution_updated")
3233
+ payload = { ...payload, partialResult: null };
3234
+ else if (payload.type === "tool_call" || payload.type === "tool_execution_started")
3235
+ payload = { ...payload, args: {} };
3236
+ }
3237
+ const frames = chatStreamFramesFromOutputEvent(payload, state, {
3148
3238
  includeRawEvent: streamId !== undefined && isPersistableOutputEvent(event.payload),
3149
3239
  });
3150
3240
  for (let index = 0; index < frames.length; index += 1) {
@@ -3153,6 +3243,7 @@ function writeChatEventFrames(controller, event, state, cursor, options = { mode
3153
3243
  const frameId = streamId === undefined ? nextTransientChatStreamFrameId(state) : `${streamId}:${index}`;
3154
3244
  writeSse(controller, "pibo", {
3155
3245
  ...frames[index],
3246
+ ...("storedPayloadRef" in event && event.storedPayloadRef ? { storedPayloadRef: event.storedPayloadRef } : {}),
3156
3247
  piboSessionId,
3157
3248
  ...(createdAt ? { createdAt } : {}),
3158
3249
  ...(!("streamId" in event) && event.replaySequence !== undefined ? { liveReplayId: event.replaySequence } : {}),
@@ -3167,81 +3258,124 @@ function chatLiveEventCreatedAt(event) {
3167
3258
  }
3168
3259
  return undefined;
3169
3260
  }
3261
+ function trackedEventStream(state, onClose) {
3262
+ const stream = new BoundedEventStream(reason => { state.boundedStreams.delete(stream); if (reason.startsWith("slow") || reason === "error")
3263
+ state.boundedStreamDisconnects++; onClose(); });
3264
+ state.boundedStreams.add(stream);
3265
+ return stream;
3266
+ }
3170
3267
  function createEventStream(input) {
3171
3268
  let unsubscribe;
3172
3269
  let heartbeat;
3173
3270
  let registeredLiveObserver = false;
3174
3271
  const streamId = randomUUID();
3175
- const stream = new ReadableStream({
3176
- start(controller) {
3177
- if (input.mode === "live" && input.activePiboSessionId) {
3178
- markEventStreamConnected(input.state, input.activePiboSessionId, streamId);
3179
- registeredLiveObserver = true;
3272
+ const stream = trackedEventStream(input.state, () => {
3273
+ unsubscribe?.();
3274
+ unsubscribe = undefined;
3275
+ if (heartbeat)
3276
+ clearInterval(heartbeat);
3277
+ heartbeat = undefined;
3278
+ if (registeredLiveObserver && input.activePiboSessionId) {
3279
+ markEventStreamDisconnected({ state: input.state, piboSessionId: input.activePiboSessionId, streamId });
3280
+ registeredLiveObserver = false;
3281
+ }
3282
+ });
3283
+ const controller = stream.writer;
3284
+ const streamState = createChatStreamState();
3285
+ let replaying = true;
3286
+ let lastReplayedStreamId = -1;
3287
+ const pendingLive = [];
3288
+ let pendingLiveBytes = 0;
3289
+ const listener = (event) => {
3290
+ if (!liveEventMatches(event, input) || stream.closed)
3291
+ return;
3292
+ if ("streamId" in event && typeof event.streamId === "number" && event.streamId <= lastReplayedStreamId)
3293
+ return;
3294
+ if (replaying) {
3295
+ try {
3296
+ pendingLiveBytes += boundedMessageBytes(event, 1024 * 1024);
3297
+ if (pendingLive.length >= 128 || pendingLiveBytes > 1024 * 1024)
3298
+ throw Error("Replay race buffer full");
3299
+ pendingLive.push(event);
3180
3300
  }
3181
- const streamState = createChatStreamState();
3182
- const transientReplay = input.mode === "live" ? collectTransientReplayEvents(input.state, {
3183
- roomId: input.roomId,
3184
- piboSessionId: input.piboSessionId,
3185
- afterReplaySequence: input.transientReplayCursor,
3186
- }) : undefined;
3187
- writeSse(controller, "pibo", {
3188
- type: "ready",
3189
- piboSessionId: input.piboSessionId ?? "",
3190
- ...(transientReplay?.status ? { liveReplay: transientReplay.status } : {}),
3191
- });
3192
- for (const stored of input.state.timelineQuery.listEvents({
3193
- roomId: input.roomId,
3194
- piboSessionId: input.piboSessionId,
3195
- afterStreamId: input.cursor ? Math.max(0, input.cursor.streamId - 1) : undefined,
3196
- limit: 1000,
3197
- })) {
3198
- writeChatEventFrames(controller, stored, streamState, input.cursor, { mode: input.mode });
3301
+ catch {
3302
+ stream.fail();
3199
3303
  }
3200
- if (input.mode === "live" && input.piboSessionId) {
3201
- if (input.transientReplayCursor === undefined) {
3202
- for (const snapshot of input.state.outputCompactor.snapshotsForSession(input.piboSessionId)) {
3203
- writeChatEventFrames(controller, { piboSessionId: snapshot.piboSessionId, eventType: snapshot.type, payload: snapshot }, streamState, undefined, { mode: input.mode });
3204
- }
3205
- }
3206
- for (const replay of transientReplay?.events ?? []) {
3207
- writeChatEventFrames(controller, replay, streamState, undefined, { mode: input.mode });
3304
+ }
3305
+ else
3306
+ writeChatEventFrames(controller, event, streamState, undefined, { mode: input.mode });
3307
+ };
3308
+ input.state.liveListeners.add(listener);
3309
+ unsubscribe = () => input.state.liveListeners.delete(listener);
3310
+ if (input.mode === "live" && input.activePiboSessionId) {
3311
+ markEventStreamConnected(input.state, input.activePiboSessionId, streamId);
3312
+ registeredLiveObserver = true;
3313
+ }
3314
+ const initialSnapshots = input.mode === "live" && input.piboSessionId ? input.state.outputCompactor.snapshotsForSession(input.piboSessionId) : [];
3315
+ void (async () => {
3316
+ const transientReplay = input.mode === "live" ? collectTransientReplayEvents(input.state, { roomId: input.roomId, piboSessionId: input.piboSessionId, afterReplaySequence: input.transientReplayCursor }) : undefined;
3317
+ writeSse(controller, "pibo", { type: "ready", piboSessionId: input.piboSessionId ?? "", ...(transientReplay?.status ? { liveReplay: transientReplay.status } : {}) });
3318
+ let afterStreamId = input.cursor ? Math.max(0, input.cursor.streamId - 1) : undefined;
3319
+ let replayed = 0;
3320
+ let pageSize = 16;
3321
+ while (!stream.closed) {
3322
+ if (!await stream.waitForCapacity())
3323
+ return;
3324
+ let events;
3325
+ try {
3326
+ events = await (input.state.readQueries?.timeline ?? input.state.timelineQuery).listEvents({ roomId: input.roomId, piboSessionId: input.piboSessionId, afterStreamId, limit: pageSize });
3327
+ }
3328
+ catch (error) {
3329
+ if (pageSize > 1 && error && typeof error === "object" && "code" in error && error.code === "storage_payload_limit") {
3330
+ pageSize = Math.max(1, Math.floor(pageSize / 2));
3331
+ continue;
3208
3332
  }
3333
+ throw error;
3209
3334
  }
3210
- const listener = (event) => {
3211
- if (!liveEventMatches(event, input))
3335
+ for (const stored of events) {
3336
+ if (!await stream.waitForCapacity())
3212
3337
  return;
3213
- writeChatEventFrames(controller, event, streamState, undefined, { mode: input.mode });
3214
- };
3215
- input.state.liveListeners.add(listener);
3216
- unsubscribe = () => {
3217
- input.state.liveListeners.delete(listener);
3218
- };
3219
- heartbeat = setInterval(() => writeSseComment(controller, "heartbeat"), 25000);
3220
- },
3221
- cancel() {
3222
- unsubscribe?.();
3223
- unsubscribe = undefined;
3224
- if (heartbeat)
3225
- clearInterval(heartbeat);
3226
- heartbeat = undefined;
3227
- if (registeredLiveObserver && input.activePiboSessionId) {
3228
- markEventStreamDisconnected({
3229
- state: input.state,
3230
- piboSessionId: input.activePiboSessionId,
3231
- streamId,
3232
- });
3233
- registeredLiveObserver = false;
3338
+ writeChatEventFrames(controller, stored, streamState, input.cursor, { mode: input.mode });
3339
+ afterStreamId = stored.streamId;
3340
+ lastReplayedStreamId = stored.streamId;
3341
+ replayed++;
3234
3342
  }
3235
- },
3236
- });
3237
- return new Response(stream, {
3238
- headers: {
3239
- "content-type": "text/event-stream; charset=utf-8",
3240
- "cache-control": "no-cache, no-transform",
3241
- "x-accel-buffering": "no",
3242
- connection: "keep-alive",
3243
- },
3244
- });
3343
+ if (events.length < pageSize)
3344
+ break;
3345
+ if (replayed >= 1000) {
3346
+ stream.finish();
3347
+ return;
3348
+ }
3349
+ }
3350
+ if (stream.closed)
3351
+ return;
3352
+ if (input.mode === "live" && input.piboSessionId) {
3353
+ if (input.transientReplayCursor === undefined)
3354
+ for (const snapshot of initialSnapshots) {
3355
+ if (!await stream.waitForCapacity())
3356
+ return;
3357
+ writeChatEventFrames(controller, { piboSessionId: snapshot.piboSessionId, eventType: snapshot.type, payload: snapshot }, streamState, undefined, { mode: input.mode });
3358
+ }
3359
+ for (const replay of transientReplay?.events ?? []) {
3360
+ if (!await stream.waitForCapacity())
3361
+ return;
3362
+ writeChatEventFrames(controller, replay, streamState, undefined, { mode: input.mode });
3363
+ }
3364
+ }
3365
+ for (const event of pendingLive) {
3366
+ if ("streamId" in event && typeof event.streamId === "number" && event.streamId <= lastReplayedStreamId)
3367
+ continue;
3368
+ if (!await stream.waitForCapacity())
3369
+ return;
3370
+ writeChatEventFrames(controller, event, streamState, undefined, { mode: input.mode });
3371
+ }
3372
+ pendingLive.length = 0;
3373
+ pendingLiveBytes = 0;
3374
+ replaying = false;
3375
+ if (!stream.closed)
3376
+ heartbeat = setInterval(() => writeSseComment(controller, "heartbeat"), 25000);
3377
+ })().catch(() => stream.fail());
3378
+ return new Response(stream.stream, { headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache, no-transform", "x-accel-buffering": "no", connection: "keep-alive" } });
3245
3379
  }
3246
3380
  function enrichWorkflowSession(state, workflowSession) {
3247
3381
  const snapshot = state.workflowService.getWorkflowSessionSnapshotForSession(workflowSession.piboSessionId);
@@ -3422,116 +3556,197 @@ function startChatStreamingFixture(input) {
3422
3556
  },
3423
3557
  });
3424
3558
  }
3425
- async function sendChatMessage(input) {
3426
- const text = normalizeMessageText(input.body.text);
3427
- const delivery = normalizeMessageDelivery(input.body.delivery);
3428
- const clientTxnId = normalizeClientTxnId(input.body.clientTxnId);
3429
- const requestedRoomId = input.forcedRoomId ?? (typeof input.body.roomId === "string" ? input.body.roomId : undefined);
3430
- const selectedSession = resolveRequestedSession(input.state, input.context, input.webSession, input.defaultProfile, typeof input.body.piboSessionId === "string" ? input.body.piboSessionId : undefined, requestedRoomId);
3431
- const room = ensureSessionRoom(input.state, input.context, selectedSession, input.webSession);
3432
- if (requestedRoomId && room.id !== requestedRoomId) {
3433
- throw new PiboWebHttpError("Session is not available in this room", 404);
3434
- }
3559
+ async function resolveAdmissionSession(storage, context, webSession, defaultProfile, piboSessionId, requestedRoomId) {
3560
+ if (piboSessionId) {
3561
+ const found = context.channelContext.getSession(piboSessionId);
3562
+ if (!found)
3563
+ throw new PiboWebHttpError("Session not found", 404);
3564
+ const session = canonicalizeSessionProfile(context, found);
3565
+ const roomId = chatRoomIdFromMetadata(session.metadata);
3566
+ const room = await storage.resolveRoom(roomId);
3567
+ if (requestedRoomId && requestedRoomId !== room.id)
3568
+ throw new PiboWebHttpError("Session is not available in this room", 404);
3569
+ if (!roomId)
3570
+ context.channelContext.updateSession?.(session.id, { metadata: withChatRoomId(session.metadata, room.id) });
3571
+ return { session: { ...session, metadata: withChatRoomId(session.metadata, room.id) }, room };
3572
+ }
3573
+ const room = await storage.resolveRoom(requestedRoomId, Boolean(requestedRoomId));
3574
+ const candidates = listSharedSessions(context);
3575
+ const existing = candidates.find(session => !session.parentId && !isChatWebSessionArchived(session) && chatRoomIdFromMetadata(session.metadata) === room.id);
3576
+ if (existing)
3577
+ return { session: existing, room };
3435
3578
  if (isPiboRoomArchived(room)) {
3436
- throw new PiboWebHttpError("Archived rooms are read-only", 403);
3579
+ const archived = candidates.find(session => !session.parentId && chatRoomIdFromMetadata(session.metadata) === room.id);
3580
+ if (archived)
3581
+ return { session: archived, room };
3582
+ throw new PiboWebHttpError("Archived room has no sessions", 404);
3437
3583
  }
3438
- input.state.sessionQuery.upsertSession(selectedSession);
3439
- const actorId = auditActorIdFor(input.webSession);
3440
- const duplicate = clientTxnId ? input.state.eventCommands.findByClientTxn(room.id, actorId, clientTxnId) : undefined;
3441
- if (duplicate)
3442
- return responseJson({ duplicate: true, event: duplicate });
3443
- const webAnnotationContext = prepareWebAnnotationAttachments({
3444
- piboSessionId: selectedSession.id,
3445
- messageText: text,
3446
- attachmentIds: input.body.webAnnotationIds,
3447
- });
3448
- const fileAttachmentContext = prepareChatFileAttachments({
3449
- messageText: webAnnotationContext.messageText,
3450
- attachmentPaths: input.body.fileAttachmentPaths,
3451
- });
3452
- const accepted = input.state.eventCommands.appendEvent({
3453
- roomId: room.id,
3454
- piboSessionId: selectedSession.id,
3455
- eventType: "user.message.accepted",
3456
- actorType: "user",
3457
- actorId,
3458
- clientTxnId,
3459
- retentionClass: "chat_message",
3460
- payload: {
3461
- type: "user.message.accepted",
3462
- piboSessionId: selectedSession.id,
3463
- roomId: room.id,
3464
- text: fileAttachmentContext.messageText,
3465
- delivery,
3466
- ...(webAnnotationContext.attachments.length ? {
3467
- webAnnotationIds: webAnnotationContext.ids,
3468
- webAnnotationAttachments: webAnnotationContext.attachments,
3469
- webAnnotationContext: webAnnotationContext.modelContext,
3470
- } : {}),
3471
- ...(fileAttachmentContext.attachments.length ? {
3472
- fileAttachmentPaths: fileAttachmentContext.paths,
3473
- fileAttachments: fileAttachmentContext.attachments,
3474
- fileAttachmentContext: fileAttachmentContext.modelContext,
3475
- } : {}),
3476
- ...(clientTxnId ? { clientTxnId } : {}),
3477
- },
3478
- });
3584
+ return { session: createSharedChatSession(context, webSession, defaultProfile, room), room };
3585
+ }
3586
+ async function sendChatMessage(input) {
3479
3587
  try {
3480
- input.state.ingestService?.ingestUserMessageAccepted({
3481
- session: selectedSession,
3482
- roomId: room.id,
3483
- actorId,
3484
- text: fileAttachmentContext.messageText,
3485
- clientTxnId,
3486
- legacyEvent: accepted,
3588
+ const startedAt = performance.now();
3589
+ const timings = [];
3590
+ const timedResponse = (value, status = 200) => responseJson(value, {
3591
+ status,
3592
+ headers: { "server-timing": [...timings, `chat_ack;dur=${(performance.now() - startedAt).toFixed(2)}`].join(", ") },
3487
3593
  });
3488
- }
3489
- catch (error) {
3490
- console.warn("V2 chat data shadow ingest failed", error);
3491
- }
3492
- for (const listener of input.state.liveListeners)
3493
- listener(accepted);
3494
- const messageId = clientTxnId ?? randomUUID();
3495
- let output;
3496
- try {
3497
- output = await input.context.channelContext.emit({
3498
- type: "message",
3594
+ const durable = input.body.admissionVersion === 2;
3595
+ if (input.body.admissionVersion !== undefined && !durable)
3596
+ throw new PiboWebHttpError("Unsupported message admission version", 400);
3597
+ if (durable && !input.state.asyncStorage)
3598
+ throw new PiboWebHttpError("Durable admission requires file-backed storage", 503);
3599
+ const text = normalizeMessageText(input.body.text);
3600
+ const delivery = normalizeMessageDelivery(input.body.delivery);
3601
+ const clientTxnId = normalizeClientTxnId(input.body.clientTxnId);
3602
+ const requestedRoomId = input.forcedRoomId ?? (typeof input.body.roomId === "string" ? input.body.roomId : undefined);
3603
+ const requestedSessionId = typeof input.body.piboSessionId === "string" ? input.body.piboSessionId : undefined;
3604
+ const resolved = input.state.asyncStorage
3605
+ ? await resolveAdmissionSession(input.state.asyncStorage, input.context, input.webSession, input.defaultProfile, requestedSessionId, requestedRoomId)
3606
+ : undefined;
3607
+ const selectedSession = resolved?.session ?? resolveRequestedSession(input.state, input.context, input.webSession, input.defaultProfile, requestedSessionId, requestedRoomId);
3608
+ const room = resolved?.room ?? ensureSessionRoom(input.state, input.context, selectedSession, input.webSession);
3609
+ if (requestedRoomId && room.id !== requestedRoomId) {
3610
+ throw new PiboWebHttpError("Session is not available in this room", 404);
3611
+ }
3612
+ if (isPiboRoomArchived(room)) {
3613
+ throw new PiboWebHttpError("Archived rooms are read-only", 403);
3614
+ }
3615
+ if (!input.state.asyncStorage)
3616
+ input.state.sessionQuery.upsertSession(selectedSession);
3617
+ const actorId = auditActorIdFor(input.webSession);
3618
+ const lookupStartedAt = performance.now();
3619
+ const duplicate = clientTxnId && !input.state.asyncStorage ? input.state.eventCommands.findByClientTxn(room.id, actorId, clientTxnId) : undefined;
3620
+ timings.push(`chat_lookup;dur=${(performance.now() - lookupStartedAt).toFixed(2)}`);
3621
+ if (duplicate)
3622
+ return timedResponse({ duplicate: true, event: duplicate });
3623
+ const webAnnotationContext = prepareWebAnnotationAttachments({
3499
3624
  piboSessionId: selectedSession.id,
3500
- id: messageId,
3501
- text: fileAttachmentContext.messageText,
3502
- delivery,
3503
- source: "user",
3625
+ messageText: text,
3626
+ attachmentIds: input.body.webAnnotationIds,
3504
3627
  });
3505
- }
3506
- catch (error) {
3507
- const errorMessage = error instanceof Error ? error.message : String(error);
3508
- if (!(error instanceof PiboSteeringUnavailableError)) {
3509
- input.context.channelContext.reportSessionError?.(selectedSession.id, errorMessage, { eventId: messageId, source: "pibo" });
3510
- }
3511
- const failed = input.state.eventCommands.appendEvent({
3628
+ const fileAttachmentContext = prepareChatFileAttachments({
3629
+ messageText: webAnnotationContext.messageText,
3630
+ attachmentPaths: input.body.fileAttachmentPaths,
3631
+ });
3632
+ const appendStartedAt = performance.now();
3633
+ const appendInput = {
3512
3634
  roomId: room.id,
3513
3635
  piboSessionId: selectedSession.id,
3514
- eventType: "user.message.failed",
3515
- actorType: "system",
3636
+ eventType: "user.message.accepted",
3637
+ actorType: "user",
3516
3638
  actorId,
3517
- retentionClass: "audit_event",
3639
+ clientTxnId,
3640
+ retentionClass: "chat_message",
3518
3641
  payload: {
3519
- type: "user.message.failed",
3642
+ type: "user.message.accepted",
3520
3643
  piboSessionId: selectedSession.id,
3521
3644
  roomId: room.id,
3645
+ text: fileAttachmentContext.messageText,
3646
+ delivery,
3647
+ ...(webAnnotationContext.attachments.length ? {
3648
+ webAnnotationIds: webAnnotationContext.ids,
3649
+ webAnnotationAttachments: webAnnotationContext.attachments,
3650
+ webAnnotationContext: webAnnotationContext.modelContext,
3651
+ } : {}),
3652
+ ...(fileAttachmentContext.attachments.length ? {
3653
+ fileAttachmentPaths: fileAttachmentContext.paths,
3654
+ fileAttachments: fileAttachmentContext.attachments,
3655
+ fileAttachmentContext: fileAttachmentContext.modelContext,
3656
+ } : {}),
3522
3657
  ...(clientTxnId ? { clientTxnId } : {}),
3523
- message: errorMessage,
3524
3658
  },
3525
- });
3659
+ };
3660
+ const messageId = clientTxnId ?? randomUUID();
3661
+ const admission = input.state.asyncStorage ? await input.state.asyncStorage.admit(appendInput, selectedSession, fileAttachmentContext.messageText, durable ? { eventId: messageId, delivery } : undefined) : undefined;
3662
+ const accepted = admission?.event ?? input.state.eventCommands.appendEvent(appendInput);
3663
+ if (admission && !admission.created)
3664
+ return timedResponse({ duplicate: true, event: accepted, ...(admission.receipt ? { receipt: admission.receipt, admissionVersion: 2, statusPath: `${CHAT_WEB_API_PREFIX}/message-receipts/${admission.receipt.id}` } : {}) }, durable ? 202 : 200);
3665
+ timings.push(`chat_append;dur=${(performance.now() - appendStartedAt).toFixed(2)}`);
3666
+ const ingestStartedAt = performance.now();
3667
+ try {
3668
+ if (!input.state.asyncStorage)
3669
+ input.state.ingestService?.ingestUserMessageAccepted({
3670
+ session: selectedSession,
3671
+ roomId: room.id,
3672
+ actorId,
3673
+ text: fileAttachmentContext.messageText,
3674
+ clientTxnId,
3675
+ legacyEvent: accepted,
3676
+ });
3677
+ }
3678
+ catch (error) {
3679
+ console.warn("V2 chat data shadow ingest failed", error);
3680
+ }
3681
+ timings.push(`chat_ingest;dur=${(performance.now() - ingestStartedAt).toFixed(2)}`);
3526
3682
  for (const listener of input.state.liveListeners)
3527
- listener(failed);
3528
- if (error instanceof PiboSteeringUnavailableError || error instanceof AgentRuntimeBindingMissingError) {
3529
- throw new PiboWebHttpError(error.message, 409);
3683
+ listener(accepted);
3684
+ if (durable && admission?.receipt) {
3685
+ input.state.commandDispatcher ??= new MessageCommandDispatcher(input.state.asyncStorage, input.context.channelContext);
3686
+ input.state.commandDispatcher.wake();
3687
+ markWebAnnotationsAttached(webAnnotationContext);
3688
+ return timedResponse({ admissionVersion: 2, receipt: admission.receipt, event: accepted, statusPath: `${CHAT_WEB_API_PREFIX}/message-receipts/${admission.receipt.id}` }, 202);
3689
+ }
3690
+ const emitStartedAt = performance.now();
3691
+ let output;
3692
+ try {
3693
+ output = await input.context.channelContext.emit({
3694
+ type: "message",
3695
+ piboSessionId: selectedSession.id,
3696
+ id: messageId,
3697
+ text: fileAttachmentContext.messageText,
3698
+ delivery,
3699
+ source: "user",
3700
+ });
3530
3701
  }
3702
+ catch (error) {
3703
+ const errorMessage = error instanceof Error ? error.message : String(error);
3704
+ if (!(error instanceof PiboSteeringUnavailableError)) {
3705
+ input.context.channelContext.reportSessionError?.(selectedSession.id, errorMessage, { eventId: messageId, source: "pibo" });
3706
+ }
3707
+ const failedInput = {
3708
+ roomId: room.id,
3709
+ piboSessionId: selectedSession.id,
3710
+ eventType: "user.message.failed",
3711
+ actorType: "system",
3712
+ actorId,
3713
+ retentionClass: "audit_event",
3714
+ payload: {
3715
+ type: "user.message.failed",
3716
+ piboSessionId: selectedSession.id,
3717
+ roomId: room.id,
3718
+ ...(clientTxnId ? { clientTxnId } : {}),
3719
+ message: errorMessage,
3720
+ },
3721
+ };
3722
+ const failed = input.state.asyncStorage ? (await input.state.asyncStorage.append(failedInput)).event : input.state.eventCommands.appendEvent(failedInput);
3723
+ for (const listener of input.state.liveListeners)
3724
+ listener(failed);
3725
+ if (error instanceof PiboSteeringUnavailableError || error instanceof AgentRuntimeBindingMissingError) {
3726
+ throw new PiboWebHttpError(error.message, 409);
3727
+ }
3728
+ throw error;
3729
+ }
3730
+ timings.push(`chat_emit;dur=${(performance.now() - emitStartedAt).toFixed(2)}`);
3731
+ markWebAnnotationsAttached(webAnnotationContext);
3732
+ return timedResponse({ output, event: accepted });
3733
+ }
3734
+ catch (error) {
3735
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
3736
+ if (code === "command_conflict")
3737
+ return responseJson({ error: "Transaction conflicts with an existing message.", code }, { status: 409 });
3738
+ if (code === "command_too_large")
3739
+ return responseJson({ error: "Message exceeds the durable command limit.", code }, { status: 413 });
3740
+ if (code === "command_overloaded")
3741
+ return responseJson({ error: "Message queue capacity reached.", code }, { status: 429, headers: { "retry-after": "1" } });
3742
+ if (code === "room_not_found")
3743
+ throw new PiboWebHttpError("Room not found", 404);
3744
+ if (code === "room_read_only")
3745
+ throw new PiboWebHttpError("Archived rooms are read-only", 403);
3746
+ if (code.startsWith("storage_"))
3747
+ return responseJson({ error: "Storage unavailable; retry with the same client transaction ID.", code, acceptanceUnknown: code === "storage_unknown" || code === "storage_operation_failed" }, { status: 503, headers: { "retry-after": "1" } });
3531
3748
  throw error;
3532
3749
  }
3533
- markWebAnnotationsAttached(webAnnotationContext);
3534
- return responseJson({ output, event: accepted });
3535
3750
  }
3536
3751
  export function createChatWebApp(options = {}) {
3537
3752
  const integrations = resolveChatWebIntegrations(options);
@@ -3550,6 +3765,9 @@ export function createChatWebApp(options = {}) {
3550
3765
  const state = {
3551
3766
  sessionQuery: new ChatSessionQueryService(dataStore),
3552
3767
  timelineQuery: new ChatTimelineQueryService(dataStore),
3768
+ boundedStreams: new Set(),
3769
+ boundedStreamDisconnects: 0,
3770
+ readQueries: dataStore.path === ":memory:" ? undefined : new AsyncChatReadQueries(dataStore.path, options.dataPayloadRootDir ?? piboHomePath("payloads")),
3553
3771
  historyQuery: new ChatHistoryQueryService(dataStore),
3554
3772
  eventCommands: new ChatEventCommandService(dataStore),
3555
3773
  readState: new ChatReadStateService(dataStore),
@@ -3560,6 +3778,7 @@ export function createChatWebApp(options = {}) {
3560
3778
  cronStore: createDefaultPiboCronStore({ path: options.cronStorePath }),
3561
3779
  loopStore: createDefaultPiboLoopStore({ path: options.ralphStorePath }),
3562
3780
  dataStore,
3781
+ asyncStorage: dataStore.path === ":memory:" ? undefined : new AsyncChatStorage(dataStore.path, options.dataPayloadRootDir ?? piboHomePath("payloads")),
3563
3782
  ingestService: new ChatDataIngestService(dataStore),
3564
3783
  traceCache: new Map(),
3565
3784
  traceTimelinePageCache: new Map(),
@@ -3589,30 +3808,49 @@ export function createChatWebApp(options = {}) {
3589
3808
  telemetryRetentionMaintenance: {},
3590
3809
  integrations,
3591
3810
  };
3811
+ const earlyTraceCache = new TraceResponseCache();
3592
3812
  let disposed = false;
3593
3813
  const requireSession = (request, context) => context.requireSession({
3594
3814
  request,
3595
3815
  });
3596
- return {
3816
+ const application = {
3597
3817
  name: CHAT_WEB_APP_NAME,
3598
3818
  mountPath: CHAT_WEB_MOUNT_PATH,
3599
3819
  apiPrefix: CHAT_WEB_API_PREFIX,
3600
- dispose() {
3820
+ initialize(context) {
3821
+ ensureCustomAgentProfiles(state, context);
3822
+ ensureEventIndexing(state, context);
3823
+ if (state.asyncStorage)
3824
+ state.commandDispatcher ??= new MessageCommandDispatcher(state.asyncStorage, context.channelContext);
3825
+ },
3826
+ async drain() {
3827
+ await state.outputPersistenceRetries.drain();
3828
+ },
3829
+ async dispose() {
3601
3830
  if (disposed)
3602
3831
  return;
3603
3832
  disposed = true;
3833
+ earlyTraceCache.clear();
3604
3834
  state.unsubscribe?.();
3605
3835
  state.unsubscribe = undefined;
3836
+ if (state.subscribedContext)
3837
+ sharedSessionSnapshots.delete(state.subscribedContext.channelContext);
3606
3838
  state.subscribedContext = undefined;
3607
3839
  state.eventLoopDelay.disable();
3840
+ await state.commandDispatcher?.dispose();
3841
+ await disposeTelemetryRetentionMaintenance(state.telemetryRetentionMaintenance);
3608
3842
  state.outputPersistenceRetries.dispose();
3609
3843
  state.workflowService.close();
3610
3844
  state.agentStore.close();
3611
3845
  state.reliabilityStore.close();
3612
3846
  state.cronStore.close();
3613
3847
  state.loopStore.close();
3848
+ for (const stream of state.boundedStreams)
3849
+ stream.fail();
3614
3850
  state.outputCompactor.disposeAll();
3615
3851
  state.outputRenderSequencer.disposeAll();
3852
+ await state.asyncStorage?.close();
3853
+ await state.readQueries?.close();
3616
3854
  state.dataStore.close();
3617
3855
  },
3618
3856
  async handleRequest(request, context) {
@@ -3678,7 +3916,7 @@ export function createChatWebApp(options = {}) {
3678
3916
  if (!nodeId || parsed.nodeId !== nodeId || parsed.payloadKind !== "output")
3679
3917
  throw new PiboWebHttpError("Trace image node does not match the payload ref", 400);
3680
3918
  resolveRequestedSession(state, context, webSession, defaultProfile, parsed.piboSessionId);
3681
- if (!state.timelineQuery.isPayloadAttachedToTraceNode({
3919
+ if (!await (state.readQueries?.timeline ?? state.timelineQuery).isPayloadAttachedToTraceNode({
3682
3920
  piboSessionId: parsed.piboSessionId,
3683
3921
  payloadId: parsed.payloadId,
3684
3922
  nodeId,
@@ -3758,6 +3996,7 @@ export function createChatWebApp(options = {}) {
3758
3996
  const requestedRoomId = url.searchParams.get("roomId") || undefined;
3759
3997
  const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined, requestedRoomId);
3760
3998
  const selectedRoomId = selectedRoomIdForSession(state, context, selectedSession);
3999
+ const structuralRevision = context.channelContext.getSessionStructureRevision?.();
3761
4000
  const ownedSessions = listSharedSessions(context);
3762
4001
  const roomSessions = visibleSessionsInRoom({
3763
4002
  state,
@@ -3769,9 +4008,14 @@ export function createChatWebApp(options = {}) {
3769
4008
  includeArchived,
3770
4009
  });
3771
4010
  const defaultRoom = state.roomService.ensureDefaultRoom();
3772
- indexSharedSessions(state.sessionQuery, roomSessions);
3773
- const sessionUnreadCounts = buildSessionUnreadCounts(state, ownedSessions);
3774
- const sessions = await buildSessionNodes(roomSessions, sessionIndexItemsWithSignalState(context, roomSessions, state.sessionQuery.listSessions(), sessionUnreadCounts), process.cwd(), sessionUnreadCounts, { skipPiMetadataFallback: true });
4011
+ const indexKey = JSON.stringify([structuralRevision, selectedRoomId, includeArchived, selectedSession.id]);
4012
+ if (structuralRevision === undefined || state.navigationIndexed?.context !== context.channelContext || state.navigationIndexed.key !== indexKey) {
4013
+ indexSharedSessions(state.sessionQuery, roomSessions);
4014
+ if (structuralRevision !== undefined)
4015
+ state.navigationIndexed = { context: context.channelContext, key: indexKey };
4016
+ }
4017
+ const sessionUnreadCounts = await buildSessionUnreadCounts(state, ownedSessions);
4018
+ const sessions = await buildSessionNodes(roomSessions, sessionIndexItemsWithSignalState(context, roomSessions, await readNavigationIndex(state, selectedRoomId), sessionUnreadCounts), process.cwd(), sessionUnreadCounts, { skipPiMetadataFallback: true });
3775
4019
  const roomTree = state.roomService.listRoomTree();
3776
4020
  const roomUnreadCounts = buildRoomUnreadCounts(ownedSessions, sessionUnreadCounts, defaultRoom.id);
3777
4021
  const rooms = roomsWithUnreadCounts(roomTree, roomUnreadCounts);
@@ -3783,7 +4027,7 @@ export function createChatWebApp(options = {}) {
3783
4027
  defaultRoomId: defaultRoom.id,
3784
4028
  selectedRoomId,
3785
4029
  selectedPiboSessionId: selectedSession.id,
3786
- latestRoomStreamId: state.timelineQuery.getLatestStreamId({ roomId: selectedRoomId }),
4030
+ latestRoomStreamId: await (state.readQueries?.timeline ?? state.timelineQuery).getLatestStreamId({ roomId: selectedRoomId }),
3787
4031
  rooms,
3788
4032
  sessions,
3789
4033
  }, { headers: { "server-timing": "navigation;desc=\"no_catalog_no_jsonl\"" } });
@@ -3795,6 +4039,7 @@ export function createChatWebApp(options = {}) {
3795
4039
  const requestedRoomId = url.searchParams.get("roomId") || undefined;
3796
4040
  const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined, requestedRoomId);
3797
4041
  const selectedRoomId = selectedRoomIdForSession(state, context, selectedSession);
4042
+ const structuralRevision = context.channelContext.getSessionStructureRevision?.();
3798
4043
  const ownedSessions = listSharedSessions(context);
3799
4044
  const roomSessions = visibleSessionsInRoom({
3800
4045
  state,
@@ -3809,10 +4054,15 @@ export function createChatWebApp(options = {}) {
3809
4054
  if (markRead) {
3810
4055
  markSessionsRead(state, sessionSubtree(ownedSessions, selectedSession.id));
3811
4056
  }
3812
- indexSharedSessions(state.sessionQuery, roomSessions);
3813
- const sessionUnreadCounts = buildSessionUnreadCounts(state, ownedSessions);
4057
+ const indexKey = JSON.stringify([structuralRevision, selectedRoomId, includeArchived, selectedSession.id]);
4058
+ if (structuralRevision === undefined || state.navigationIndexed?.context !== context.channelContext || state.navigationIndexed.key !== indexKey) {
4059
+ indexSharedSessions(state.sessionQuery, roomSessions);
4060
+ if (structuralRevision !== undefined)
4061
+ state.navigationIndexed = { context: context.channelContext, key: indexKey };
4062
+ }
4063
+ const sessionUnreadCounts = await buildSessionUnreadCounts(state, ownedSessions);
3814
4064
  const [sessions, catalog] = await Promise.all([
3815
- buildSessionNodes(roomSessions, sessionIndexItemsWithSignalState(context, roomSessions, state.sessionQuery.listSessions(), sessionUnreadCounts), process.cwd(), sessionUnreadCounts, sessionNodeHistoryOptions(context)),
4065
+ buildSessionNodes(roomSessions, sessionIndexItemsWithSignalState(context, roomSessions, await readNavigationIndex(state, selectedRoomId), sessionUnreadCounts), process.cwd(), sessionUnreadCounts, sessionNodeHistoryOptions(context)),
3816
4066
  loadBootstrapCatalog(state, context, webSession),
3817
4067
  ]);
3818
4068
  const roomTree = state.roomService.listRoomTree();
@@ -3825,7 +4075,7 @@ export function createChatWebApp(options = {}) {
3825
4075
  room: state.roomService.getRoom(selectedRoomId),
3826
4076
  selectedRoomId,
3827
4077
  selectedPiboSessionId: selectedSession.id,
3828
- latestRoomStreamId: state.timelineQuery.getLatestStreamId({ roomId: selectedRoomId }),
4078
+ latestRoomStreamId: await (state.readQueries?.timeline ?? state.timelineQuery).getLatestStreamId({ roomId: selectedRoomId }),
3829
4079
  rooms,
3830
4080
  sessions,
3831
4081
  ...catalog,
@@ -3943,29 +4193,32 @@ export function createChatWebApp(options = {}) {
3943
4193
  let unsubscribe;
3944
4194
  let heartbeat;
3945
4195
  let closed = false;
3946
- const stream = new ReadableStream({
3947
- start: (controller) => {
3948
- writeJsonSse(controller, "signal_status_snapshot", context.channelContext.snapshotSignalStatuses());
3949
- unsubscribe = context.channelContext.subscribeSignalStatuses((patch) => {
3950
- if (closed)
3951
- return;
3952
- const statusPatch = compactSignalStatusPatch(patch);
3953
- writeJsonSse(controller, "signal_status_patch", statusPatch, `${patch.rootPiboSessionId}:${patch.toVersion}`);
3954
- });
3955
- heartbeat = setInterval(() => {
3956
- if (!closed)
3957
- writeSseComment(controller, "heartbeat");
3958
- }, 25_000);
3959
- },
3960
- cancel: () => {
3961
- closed = true;
3962
- unsubscribe?.();
3963
- unsubscribe = undefined;
3964
- if (heartbeat)
3965
- clearInterval(heartbeat);
3966
- heartbeat = undefined;
3967
- },
3968
- });
4196
+ const cleanup = () => {
4197
+ closed = true;
4198
+ unsubscribe?.();
4199
+ unsubscribe = undefined;
4200
+ if (heartbeat)
4201
+ clearInterval(heartbeat);
4202
+ heartbeat = undefined;
4203
+ };
4204
+ const bounded = trackedEventStream(state, cleanup);
4205
+ {
4206
+ const controller = bounded.writer;
4207
+ writeJsonSse(controller, "signal_status_snapshot", context.channelContext.snapshotSignalStatuses());
4208
+ unsubscribe = context.channelContext.subscribeSignalStatuses((patch) => {
4209
+ if (closed)
4210
+ return;
4211
+ const statusPatch = compactSignalStatusPatch(patch);
4212
+ writeJsonSse(controller, "signal_status_patch", statusPatch, `${patch.rootPiboSessionId}:${patch.toVersion}`);
4213
+ });
4214
+ heartbeat = setInterval(() => {
4215
+ if (!closed)
4216
+ writeSseComment(controller, "heartbeat");
4217
+ }, 25_000);
4218
+ }
4219
+ if (closed)
4220
+ cleanup();
4221
+ const stream = bounded.stream;
3969
4222
  return new Response(stream, {
3970
4223
  headers: signalSseHeaders(),
3971
4224
  });
@@ -3997,38 +4250,41 @@ export function createChatWebApp(options = {}) {
3997
4250
  let unsubscribeStatuses;
3998
4251
  let heartbeat;
3999
4252
  let closed = false;
4000
- const stream = new ReadableStream({
4001
- start: (controller) => {
4002
- writeJsonSse(controller, "signal_snapshot", context.channelContext.snapshotSignalTree(rootPiboSessionId));
4003
- unsubscribeTree = context.channelContext.subscribeSignalTree(rootPiboSessionId, (patch) => {
4004
- if (!closed)
4005
- writeJsonSse(controller, "signal_patch", patch, String(patch.toVersion));
4253
+ const cleanup = () => {
4254
+ closed = true;
4255
+ unsubscribeTree?.();
4256
+ unsubscribeTree = undefined;
4257
+ unsubscribeStatuses?.();
4258
+ unsubscribeStatuses = undefined;
4259
+ if (heartbeat)
4260
+ clearInterval(heartbeat);
4261
+ heartbeat = undefined;
4262
+ };
4263
+ const bounded = trackedEventStream(state, cleanup);
4264
+ {
4265
+ const controller = bounded.writer;
4266
+ writeJsonSse(controller, "signal_snapshot", context.channelContext.snapshotSignalTree(rootPiboSessionId));
4267
+ unsubscribeTree = context.channelContext.subscribeSignalTree(rootPiboSessionId, (patch) => {
4268
+ if (!closed)
4269
+ writeJsonSse(controller, "signal_patch", patch, String(patch.toVersion));
4270
+ });
4271
+ if (includeStatuses) {
4272
+ writeJsonSse(controller, "signal_status_snapshot", context.channelContext.snapshotSignalStatuses());
4273
+ unsubscribeStatuses = context.channelContext.subscribeSignalStatuses((patch) => {
4274
+ if (closed)
4275
+ return;
4276
+ const statusPatch = compactSignalStatusPatch(patch);
4277
+ writeJsonSse(controller, "signal_status_patch", statusPatch, `${patch.rootPiboSessionId}:${patch.toVersion}`);
4006
4278
  });
4007
- if (includeStatuses) {
4008
- writeJsonSse(controller, "signal_status_snapshot", context.channelContext.snapshotSignalStatuses());
4009
- unsubscribeStatuses = context.channelContext.subscribeSignalStatuses((patch) => {
4010
- if (closed)
4011
- return;
4012
- const statusPatch = compactSignalStatusPatch(patch);
4013
- writeJsonSse(controller, "signal_status_patch", statusPatch, `${patch.rootPiboSessionId}:${patch.toVersion}`);
4014
- });
4015
- }
4016
- heartbeat = setInterval(() => {
4017
- if (!closed)
4018
- writeSseComment(controller, "heartbeat");
4019
- }, 25_000);
4020
- },
4021
- cancel: () => {
4022
- closed = true;
4023
- unsubscribeTree?.();
4024
- unsubscribeTree = undefined;
4025
- unsubscribeStatuses?.();
4026
- unsubscribeStatuses = undefined;
4027
- if (heartbeat)
4028
- clearInterval(heartbeat);
4029
- heartbeat = undefined;
4030
- },
4031
- });
4279
+ }
4280
+ heartbeat = setInterval(() => {
4281
+ if (!closed)
4282
+ writeSseComment(controller, "heartbeat");
4283
+ }, 25_000);
4284
+ }
4285
+ if (closed)
4286
+ cleanup();
4287
+ const stream = bounded.stream;
4032
4288
  return new Response(stream, {
4033
4289
  headers: signalSseHeaders(),
4034
4290
  });
@@ -4653,13 +4909,22 @@ export function createChatWebApp(options = {}) {
4653
4909
  const webSession = await requireSession(request, context);
4654
4910
  requireRoom(state, roomResource.roomId, webSession, "read");
4655
4911
  const cursor = parseSseCursor(url.searchParams.get("since"));
4656
- return responseJson({
4657
- events: state.timelineQuery.listEvents({
4658
- roomId: roomResource.roomId,
4659
- afterStreamId: cursor?.streamId,
4660
- limit: 1000,
4661
- }),
4662
- });
4912
+ let limit = 1000;
4913
+ for (;;) {
4914
+ try {
4915
+ const events = await (state.readQueries?.timeline ?? state.timelineQuery).listEvents({
4916
+ roomId: roomResource.roomId,
4917
+ afterStreamId: cursor?.streamId,
4918
+ limit,
4919
+ });
4920
+ return responseJson({ events });
4921
+ }
4922
+ catch (error) {
4923
+ if (limit <= 1 || !error || typeof error !== "object" || !("code" in error) || error.code !== "storage_payload_limit")
4924
+ throw error;
4925
+ limit = Math.max(1, Math.floor(limit / 2));
4926
+ }
4927
+ }
4663
4928
  }
4664
4929
  if (roomResource && roomResource.child === "messages" && request.method === "POST") {
4665
4930
  requireSameOriginJsonRequest(request);
@@ -4861,16 +5126,16 @@ export function createChatWebApp(options = {}) {
4861
5126
  const startedAt = performance.now();
4862
5127
  const webSession = await requireSession(request, context);
4863
5128
  const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined);
4864
- state.sessionQuery.upsertSession(selectedSession);
5129
+ state.sessionQuery.upsertSessionsIfChanged([selectedSession]);
4865
5130
  const indexedSession = state.sessionQuery.getSession(selectedSession.id);
4866
5131
  const historyStartedAt = performance.now();
4867
- const productHistory = state.historyQuery.getProductHistoryCoverage(selectedSession.id);
5132
+ const productHistory = await (state.readQueries?.history ?? state.historyQuery).getProductHistoryCoverage(selectedSession.id);
4868
5133
  const historyInspection = requiresNativeHistoryCompatibility(selectedSession) && context.channelContext.inspectSessionRuntimeHistory
4869
5134
  ? await context.channelContext.inspectSessionRuntimeHistory(selectedSession.id).catch(() => undefined)
4870
5135
  : undefined;
4871
5136
  const historyMs = performance.now() - historyStartedAt;
4872
- const lastEventSequence = state.timelineQuery.getLatestEventSequence(selectedSession.id);
4873
- const latestStreamId = state.timelineQuery.getLatestStreamId({ piboSessionId: selectedSession.id });
5137
+ const lastEventSequence = await (state.readQueries?.timeline ?? state.timelineQuery).getLatestEventSequence(selectedSession.id);
5138
+ const latestStreamId = await (state.readQueries?.timeline ?? state.timelineQuery).getLatestStreamId({ piboSessionId: selectedSession.id });
4874
5139
  const version = createFastTraceV2Version({
4875
5140
  session: selectedSession,
4876
5141
  sessions: listSharedSessions(context),
@@ -4914,14 +5179,14 @@ export function createChatWebApp(options = {}) {
4914
5179
  const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined);
4915
5180
  if (timelineCursor.kind === "history")
4916
5181
  validateRuntimeHistoryCursor(selectedSession, timelineCursor);
4917
- state.sessionQuery.upsertSession(selectedSession);
5182
+ state.sessionQuery.upsertSessionsIfChanged([selectedSession]);
4918
5183
  const ownedSessions = listSharedSessions(context);
4919
5184
  const indexedSession = state.sessionQuery.getSession(selectedSession.id);
4920
5185
  let historyMs = 0;
4921
- const productHistory = state.historyQuery.getProductHistoryCoverage(selectedSession.id);
4922
- const lastEventSequence = state.timelineQuery.getLatestEventSequence(selectedSession.id);
4923
- const latestStreamId = state.timelineQuery.getLatestStreamId({ piboSessionId: selectedSession.id });
4924
- const turnTimingScan = state.timelineQuery.scanMessageTurnTimings(selectedSession.id);
5186
+ const productHistory = await (state.readQueries?.history ?? state.historyQuery).getProductHistoryCoverage(selectedSession.id);
5187
+ const lastEventSequence = await (state.readQueries?.timeline ?? state.timelineQuery).getLatestEventSequence(selectedSession.id);
5188
+ const latestStreamId = await (state.readQueries?.timeline ?? state.timelineQuery).getLatestStreamId({ piboSessionId: selectedSession.id });
5189
+ const turnTimingScan = await (state.readQueries?.timeline ?? state.timelineQuery).scanMessageTurnTimings(selectedSession.id);
4925
5190
  const turnTimings = turnTimingScan.timings;
4926
5191
  const liveSnapshots = timelineCursor.kind === "tail" ? state.outputCompactor.snapshotsForSession(selectedSession.id) : [];
4927
5192
  const runtimeStatus = context.channelContext.getSessionRuntimeStatus
@@ -5023,7 +5288,7 @@ export function createChatWebApp(options = {}) {
5023
5288
  };
5024
5289
  }
5025
5290
  else {
5026
- const events = state.timelineQuery.listTraceEvents({
5291
+ const events = await (state.readQueries?.timeline ?? state.timelineQuery).listTraceEvents({
5027
5292
  piboSessionId: selectedSession.id,
5028
5293
  limit,
5029
5294
  ...(beforeSequence !== undefined ? { beforeSequence } : {}),
@@ -5041,7 +5306,7 @@ export function createChatWebApp(options = {}) {
5041
5306
  }
5042
5307
  const historyEntries = nativeHistory?.entries.length
5043
5308
  ? nativeHistory.entries
5044
- : state.historyQuery.listProductHistoryEntries({
5309
+ : await (state.readQueries?.history ?? state.historyQuery).listProductHistoryEntries({
5045
5310
  piboSessionId: selectedSession.id,
5046
5311
  limit: Math.min(limit * 2, 500),
5047
5312
  ...(beforeSequence !== undefined ? { beforeSequence } : {}),
@@ -5124,9 +5389,19 @@ export function createChatWebApp(options = {}) {
5124
5389
  if (!parsed)
5125
5390
  throw new PiboWebHttpError("Invalid trace payload ref", 400);
5126
5391
  resolveRequestedSession(state, context, webSession, defaultProfile, parsed.piboSessionId);
5392
+ if (url.searchParams.get("download") === "1") {
5393
+ const payload = state.dataStore.payloads.getPayload(parsed.payloadId);
5394
+ if (!payload)
5395
+ throw new PiboWebHttpError("Trace payload not found", 404);
5396
+ const body = Readable.toWeb(state.dataStore.payloads.openPayloadStream(parsed.payloadId));
5397
+ return new Response(body, { headers: {
5398
+ "content-type": "application/octet-stream", "content-disposition": 'attachment; filename="message-content.txt"',
5399
+ "content-length": String(payload.byteSize), "cache-control": "no-store", "x-content-type-options": "nosniff",
5400
+ } });
5401
+ }
5127
5402
  const offset = parseNonNegativeIntSearchParam(url, "offset", 0, Number.MAX_SAFE_INTEGER);
5128
5403
  const limit = parsePositiveIntSearchParam(url, "limit", TRACE_V2_PAYLOAD_DEFAULT_LIMIT_BYTES, TRACE_V2_PAYLOAD_MAX_LIMIT_BYTES);
5129
- const chunk = readTracePayloadChunk({ payloadStore: state.dataStore.payloads, ref, offset, limit });
5404
+ const chunk = await readTracePayloadChunk({ payloadStore: state.dataStore.payloads, ref, offset, limit });
5130
5405
  if (!chunk)
5131
5406
  throw new PiboWebHttpError("Trace payload not found", 404);
5132
5407
  return responseJson(chunk, { headers: { "cache-control": "no-store" } });
@@ -5139,7 +5414,7 @@ export function createChatWebApp(options = {}) {
5139
5414
  const beforeSequence = rawCursor.kind === "event" ? rawCursor.beforeSequence : undefined;
5140
5415
  const limit = parsePositiveIntSearchParam(url, "limit", TRACE_V2_RAW_EVENTS_DEFAULT_LIMIT, TRACE_V2_RAW_EVENTS_MAX_LIMIT);
5141
5416
  const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined);
5142
- const events = state.timelineQuery.listTraceEvents({
5417
+ const events = await (state.readQueries?.timeline ?? state.timelineQuery).listTraceEvents({
5143
5418
  piboSessionId: selectedSession.id,
5144
5419
  limit,
5145
5420
  ...(beforeSequence !== undefined ? { beforeSequence } : {}),
@@ -5161,11 +5436,11 @@ export function createChatWebApp(options = {}) {
5161
5436
  ? parsePositiveIntSearchParam(url, "pageSize", DEFAULT_TRACE_EVENTS_PAGE_SIZE, TRACE_V1_COMPAT_MAX_EVENTS_PER_REQUEST)
5162
5437
  : parsePositiveIntSearchParam(url, "eventLimit", DEFAULT_TRACE_EVENTS_PAGE_SIZE, TRACE_V1_COMPAT_MAX_EVENTS_PER_REQUEST);
5163
5438
  const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined);
5164
- state.sessionQuery.upsertSession(selectedSession);
5439
+ state.sessionQuery.upsertSessionsIfChanged([selectedSession]);
5165
5440
  const ownedSessions = listSharedSessions(context);
5166
5441
  const indexedSession = state.sessionQuery.getSession(selectedSession.id);
5167
5442
  let historyMs = 0;
5168
- const productHistory = state.historyQuery.getProductHistoryCoverage(selectedSession.id);
5443
+ const productHistory = await (state.readQueries?.history ?? state.historyQuery).getProductHistoryCoverage(selectedSession.id);
5169
5444
  let nativeHistory;
5170
5445
  if (beforeSequence === undefined
5171
5446
  && requiresNativeHistoryCompatibility(selectedSession)
@@ -5175,9 +5450,9 @@ export function createChatWebApp(options = {}) {
5175
5450
  nativeHistory = await context.channelContext.readSessionRuntimeHistory(selectedSession.id, { limit: Math.min(eventLimit * 4, 500) }).catch(() => undefined);
5176
5451
  historyMs += performance.now() - historyStartedAt;
5177
5452
  }
5178
- const lastEventSequence = state.timelineQuery.getLatestEventSequence(selectedSession.id);
5179
- const latestStreamId = state.timelineQuery.getLatestStreamId({ piboSessionId: selectedSession.id });
5180
- const turnTimingScan = state.timelineQuery.scanMessageTurnTimings(selectedSession.id);
5453
+ const lastEventSequence = await (state.readQueries?.timeline ?? state.timelineQuery).getLatestEventSequence(selectedSession.id);
5454
+ const latestStreamId = await (state.readQueries?.timeline ?? state.timelineQuery).getLatestStreamId({ piboSessionId: selectedSession.id });
5455
+ const turnTimingScan = await (state.readQueries?.timeline ?? state.timelineQuery).scanMessageTurnTimings(selectedSession.id);
5181
5456
  const turnTimings = turnTimingScan.timings;
5182
5457
  const liveSnapshots = beforeSequence === undefined ? state.outputCompactor.snapshotsForSession(selectedSession.id) : [];
5183
5458
  const runtimeStatus = context.channelContext.getSessionRuntimeStatus
@@ -5214,7 +5489,7 @@ export function createChatWebApp(options = {}) {
5214
5489
  let trace = cached;
5215
5490
  let eventCount = 0;
5216
5491
  if (!trace) {
5217
- const events = state.timelineQuery.listTraceEvents({
5492
+ const events = await (state.readQueries?.timeline ?? state.timelineQuery).listTraceEvents({
5218
5493
  piboSessionId: selectedSession.id,
5219
5494
  limit: eventLimit,
5220
5495
  ...(beforeSequence !== undefined ? { beforeSequence } : {}),
@@ -5222,7 +5497,7 @@ export function createChatWebApp(options = {}) {
5222
5497
  eventCount = events.length;
5223
5498
  const historyEntries = nativeHistory?.entries.length
5224
5499
  ? nativeHistory.entries
5225
- : state.historyQuery.listProductHistoryEntries({
5500
+ : await (state.readQueries?.history ?? state.historyQuery).listProductHistoryEntries({
5226
5501
  piboSessionId: selectedSession.id,
5227
5502
  limit: Math.min(eventLimit * 2, 1000),
5228
5503
  ...(beforeSequence !== undefined ? { beforeSequence } : {}),
@@ -5261,7 +5536,7 @@ export function createChatWebApp(options = {}) {
5261
5536
  }, { status: 413, headers: { ...baseHeaders, "x-pibo-trace-v1-deprecated": "true", ...serverTiming(cached ? "hit" : "miss", eventCount) } });
5262
5537
  }
5263
5538
  if (includeRawEvents) {
5264
- const rawEvents = state.timelineQuery.listTraceEvents({
5539
+ const rawEvents = await (state.readQueries?.timeline ?? state.timelineQuery).listTraceEvents({
5265
5540
  piboSessionId: selectedSession.id,
5266
5541
  limit: rawEventsLimit,
5267
5542
  ...(beforeSequence !== undefined ? { beforeSequence } : {}),
@@ -5281,7 +5556,10 @@ export function createChatWebApp(options = {}) {
5281
5556
  }
5282
5557
  if (url.pathname === `${CHAT_WEB_API_PREFIX}/debug/resources` && request.method === "GET") {
5283
5558
  await requireSession(request, context);
5284
- return responseJson({ gateway: serializeGatewayResourceDiagnostics(state) }, { headers: { "cache-control": "no-store" } });
5559
+ return responseJson({
5560
+ gateway: serializeGatewayResourceDiagnostics(state),
5561
+ storage: state.asyncStorage?.status() ?? { ready: true, mode: "in-process" },
5562
+ }, { headers: { "cache-control": "no-store" } });
5285
5563
  }
5286
5564
  if (url.pathname === `${CHAT_WEB_API_PREFIX}/debug/trace-at-sequence` && request.method === "POST") {
5287
5565
  requireSameOriginJsonRequest(request);
@@ -5297,12 +5575,12 @@ export function createChatWebApp(options = {}) {
5297
5575
  throw new PiboWebHttpError("Session not found", 404);
5298
5576
  const ownedSessions = listSharedSessions(context);
5299
5577
  const indexedSession = state.sessionQuery.getSession(piboSessionId);
5300
- const turnTimingScan = state.timelineQuery.scanMessageTurnTimings(piboSessionId);
5578
+ const turnTimingScan = await (state.readQueries?.timeline ?? state.timelineQuery).scanMessageTurnTimings(piboSessionId);
5301
5579
  const trace = await buildTraceView({
5302
5580
  session,
5303
5581
  sessions: ownedSessions,
5304
- events: state.timelineQuery.listTraceEvents({ piboSessionId, beforeOrAtSequence: eventSequence, limit: DEFAULT_TRACE_EVENTS_PAGE_SIZE }),
5305
- historyEntries: state.historyQuery.listProductHistoryEntries({
5582
+ events: await (state.readQueries?.timeline ?? state.timelineQuery).listTraceEvents({ piboSessionId, beforeOrAtSequence: eventSequence, limit: DEFAULT_TRACE_EVENTS_PAGE_SIZE }),
5583
+ historyEntries: await (state.readQueries?.history ?? state.historyQuery).listProductHistoryEntries({
5306
5584
  piboSessionId,
5307
5585
  limit: DEFAULT_TRACE_EVENTS_PAGE_SIZE,
5308
5586
  beforeSequence: eventSequence + 1,
@@ -5319,6 +5597,23 @@ export function createChatWebApp(options = {}) {
5319
5597
  const body = await readJsonBody(request);
5320
5598
  return startChatStreamingFixture({ state, context, webSession, defaultProfile, body });
5321
5599
  }
5600
+ if (url.pathname === `${CHAT_WEB_API_PREFIX}/message-receipts` && request.method === "GET") {
5601
+ const webSession = await requireSession(request, context);
5602
+ const sessionId = url.searchParams.get("piboSessionId") ?? "";
5603
+ if (!sessionId)
5604
+ throw new PiboWebHttpError("Session ID required", 400);
5605
+ resolveRequestedSession(state, context, webSession, defaultProfile, sessionId);
5606
+ return responseJson(await state.asyncStorage?.commandReceiptPage(sessionId) ?? { receipts: [] }, { headers: { "cache-control": "no-store" } });
5607
+ }
5608
+ if (url.pathname.startsWith(`${CHAT_WEB_API_PREFIX}/message-receipts/`) && request.method === "GET") {
5609
+ const webSession = await requireSession(request, context);
5610
+ const id = decodeURIComponent(url.pathname.slice(`${CHAT_WEB_API_PREFIX}/message-receipts/`.length));
5611
+ const receipt = await state.asyncStorage?.commandReceipt(id);
5612
+ if (!receipt)
5613
+ throw new PiboWebHttpError("Message receipt not found", 404);
5614
+ resolveRequestedSession(state, context, webSession, defaultProfile, receipt.sessionId, receipt.roomId);
5615
+ return responseJson({ receipt }, { headers: { "cache-control": "no-store" } });
5616
+ }
5322
5617
  if (url.pathname === `${CHAT_WEB_API_PREFIX}/message` && request.method === "POST") {
5323
5618
  requireSameOriginJsonRequest(request);
5324
5619
  const webSession = await requireSession(request, context);
@@ -5359,7 +5654,7 @@ export function createChatWebApp(options = {}) {
5359
5654
  if (!context.channelContext.getSessionStatusSnapshot) {
5360
5655
  throw new PiboWebHttpError("Session status snapshots are not available", 501);
5361
5656
  }
5362
- state.sessionQuery.upsertSession(selectedSession);
5657
+ state.sessionQuery.upsertSessionsIfChanged([selectedSession]);
5363
5658
  const snapshot = await context.channelContext.getSessionStatusSnapshot(selectedSession.id, url.searchParams.get("activate") === "false" ? { activate: false } : undefined);
5364
5659
  return responseJson(snapshot ?? { piboSessionId: selectedSession.id, runtimeActive: false }, {
5365
5660
  headers: { "cache-control": "no-store" },
@@ -5407,12 +5702,14 @@ export function createChatWebApp(options = {}) {
5407
5702
  if (isPiboRoomArchived(room)) {
5408
5703
  throw new PiboWebHttpError("Archived rooms are read-only", 403);
5409
5704
  }
5410
- state.sessionQuery.upsertSession(selectedSession);
5705
+ state.sessionQuery.upsertSessionsIfChanged([selectedSession]);
5706
+ const cancelledPending = body.action === "clear_queue" ? await state.asyncStorage?.cancelPendingCommands(selectedSession.id) ?? 0 : 0;
5411
5707
  const output = await context.channelContext.emit({
5412
5708
  type: "execution",
5413
5709
  piboSessionId: selectedSession.id,
5414
5710
  id: randomUUID(),
5415
5711
  action: body.action,
5712
+ ...(cancelledPending ? { clearedBeforeRuntime: cancelledPending } : {}),
5416
5713
  ...(body.params === undefined ? {} : { params: body.params }),
5417
5714
  });
5418
5715
  return responseJson(output);
@@ -5442,4 +5739,34 @@ export function createChatWebApp(options = {}) {
5442
5739
  return undefined;
5443
5740
  },
5444
5741
  };
5742
+ const uncachedHandle = application.handleRequest.bind(application);
5743
+ application.handleRequest = async (request, context) => {
5744
+ const url = new URL(request.url);
5745
+ const sid = url.searchParams.get("piboSessionId");
5746
+ const structure = context.channelContext.getSessionStructureRevision?.();
5747
+ if (request.method !== "GET" || !sid || structure === undefined || ![`${CHAT_WEB_API_PREFIX}/trace`, `${CHAT_WEB_API_PREFIX}/trace/summary`, `${CHAT_WEB_API_PREFIX}/trace/timeline`].includes(url.pathname))
5748
+ return uncachedHandle(request, context);
5749
+ const webSession = await requireSession(request, context);
5750
+ const selected = resolveRequestedSession(state, context, webSession, defaultProfile, sid);
5751
+ if (requiresNativeHistoryCompatibility(selected))
5752
+ return uncachedHandle(request, context);
5753
+ url.searchParams.sort();
5754
+ const fingerprint = () => {
5755
+ const session = context.channelContext.getSession(sid);
5756
+ const revision = state.dataStore.db.prepare("SELECT revision FROM chat_trace_revisions WHERE session_id=?").get(sid);
5757
+ const history = state.dataStore.db.prepare("SELECT revision FROM chat_history_counts WHERE session_id=?").get(sid);
5758
+ const backfill = state.dataStore.db.prepare("SELECT cursor,target FROM chat_read_backfill WHERE id=1").get();
5759
+ const runtime = context.channelContext.getSessionRuntimeStatus?.(sid);
5760
+ return JSON.stringify({ url: url.href, user: webSession.authSession.identity.userId, encoding: request.headers.get("accept-encoding"), structure: context.channelContext.getSessionStructureRevision?.(), session, revision, history, backfill, live: state.outputCompactor.versionForSession(sid), runtime: runtime ? { processing: runtime.processing, streaming: runtime.streaming, queuedMessages: runtime.queuedMessages } : null });
5761
+ };
5762
+ const key = fingerprint();
5763
+ const cached = earlyTraceCache.get(key, request);
5764
+ if (cached)
5765
+ return cached;
5766
+ const response = await uncachedHandle(request, context);
5767
+ if (response && fingerprint() === key)
5768
+ await earlyTraceCache.set(key, response);
5769
+ return response;
5770
+ };
5771
+ return application;
5445
5772
  }