@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
@@ -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";
@@ -33,7 +41,7 @@ import { listMcpServerInfos } from "../../mcp/agent-context.js";
33
41
  import { getDefaultPiboWorkspace } from "../../core/workspace.js";
34
42
  import { findPiPackage, listPiPackages } from "../../pi-packages/store.js";
35
43
  import { ScopedUserSkillManager } from "../../user-skills/manager.js";
36
- import { ChatDataIngestService, outputIdempotencyKey, outputPersistenceDeliveryKey, outputPersistenceErrorIsRetryable } from "../../data/ingest-service.js";
44
+ import { ChatDataIngestService, legacyOutputIdempotencyKey, outputIdempotencyKey, outputPersistenceDeliveryKey, outputPersistenceErrorIsRetryable } from "../../data/ingest-service.js";
37
45
  import { ChatEventCommandService } from "./data/event-command-service.js";
38
46
  import { ChatReadStateService } from "./data/read-state-service.js";
39
47
  import { ChatRoomService, PiboRoomHierarchyCycleError } from "./data/room-service.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,37 @@ 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
+ persistenceProvenance: {
540
+ producer: "chat-web",
541
+ projection: "product-history",
542
+ phase: retryContext.attempt > 1 ? "durable-replay" : "live",
543
+ },
544
+ };
545
+ const asyncIngested = state.asyncStorage ? await state.asyncStorage.ingestOutput(ingestInput) : undefined;
546
+ const ingested = asyncIngested ?? state.ingestService.ingestOutputEvent(ingestInput);
547
+ const storedEvent = asyncIngested?.stored ?? state.dataStore.eventLog.findByIdempotencyKey(delivery.deliveryId);
548
+ if (!storedEvent || (!asyncIngested && "streamId" in storedEvent && storedEvent.streamId !== ingested.streamId)) {
519
549
  throw new Error(`Missing V2 event ${ingested.streamId} for ${delivery.deliveryId}`);
520
550
  }
521
551
  delivery.v2 = {
522
552
  streamId: ingested.streamId,
523
553
  createdAt: storedEvent.createdAt,
524
- eventId: eventIdentityForDelivery(delivery.event),
554
+ eventId: asyncIngested?.stored.eventId ?? eventIdentityForDelivery(delivery.event),
525
555
  duplicate: ingested.duplicate,
526
556
  };
527
557
  checkpoint();
528
558
  }
559
+ state.commandDispatcher?.outputPersisted(delivery.event);
529
560
  if (!delivery.reliabilityDelivered) {
530
561
  if (delivery.reliabilityPayload === undefined) {
531
562
  delivery.reliabilityPayload = boundedReliabilityOutputPayload(state, delivery.event);
532
- checkpoint();
563
+ // Preparation is replayable; checkpoint together with the confirmed append below.
533
564
  }
534
565
  const deliveryKey = delivery.deliveryId;
535
566
  state.reliabilityStore.appendOnce({
@@ -545,10 +576,19 @@ function deliverWebOutputPersistenceState(state, context, retryContext) {
545
576
  }
546
577
  if (!delivery.sideEffectsDelivered && state.reliabilityStore.hasDeliveryReceipt(delivery.deliveryId, "chat-web-observable-v1")) {
547
578
  delivery.sideEffectsDelivered = true;
548
- checkpoint();
579
+ // The durable receipt is authoritative; the job can now finish without another payload rewrite.
549
580
  }
550
581
  else if (!delivery.sideEffectsDelivered) {
551
- const stored = storedChatEventForDelivery(persistenceState, delivery);
582
+ let stored = storedChatEventForDelivery(persistenceState, delivery);
583
+ try {
584
+ boundedMessageBytes(stored.payload, 64 * 1024);
585
+ }
586
+ catch {
587
+ const [persisted] = await (state.readQueries?.timeline ?? state.timelineQuery).listEvents({ piboSessionId: stored.piboSessionId, afterStreamId: stored.streamId - 1, limit: 1 });
588
+ if (!persisted || persisted.streamId !== stored.streamId)
589
+ throw new Error("Persisted output unavailable for live delivery");
590
+ stored = persisted;
591
+ }
552
592
  if (delivery.event.type === "assistant_message" || delivery.event.type === "message_finished" || delivery.event.type === "session_error") {
553
593
  markActiveSessionRead(state, delivery.event.piboSessionId, stored.streamId);
554
594
  }
@@ -566,7 +606,7 @@ function deliverWebOutputPersistenceState(state, context, retryContext) {
566
606
  // recording before sends would trade duplicates for silent loss.
567
607
  state.reliabilityStore.recordDeliveryReceipt(delivery.deliveryId, "chat-web-observable-v1");
568
608
  delivery.sideEffectsDelivered = true;
569
- checkpoint();
609
+ // The durable receipt is authoritative; the job can now finish without another payload rewrite.
570
610
  }
571
611
  }
572
612
  catch (error) {
@@ -625,7 +665,7 @@ function parseWebOutputPersistenceState(value) {
625
665
  if (!isPiboOutputEvent(delivery.event) || !outputIdempotencyKey(delivery.event))
626
666
  return undefined;
627
667
  const deliveryId = outputPersistenceDeliveryKey(delivery.event);
628
- if (delivery.deliveryId !== undefined && delivery.deliveryId !== deliveryId)
668
+ if (delivery.deliveryId !== undefined && delivery.deliveryId !== deliveryId && delivery.deliveryId !== legacyOutputIdempotencyKey(delivery.event))
629
669
  return undefined;
630
670
  const v2 = delivery.v2;
631
671
  if (v2 !== undefined && (!v2 || typeof v2 !== "object" || Array.isArray(v2)
@@ -754,12 +794,45 @@ function markActiveSessionRead(state, piboSessionId, streamId) {
754
794
  return;
755
795
  state.readState.markSessionRead(piboSessionId, streamId);
756
796
  }
797
+ async function readNavigationIndex(state, roomId) {
798
+ if (!state.readQueries)
799
+ return state.sessionQuery.listSessions(roomId);
800
+ const items = [];
801
+ let afterId;
802
+ for (;;) {
803
+ const page = await state.readQueries.navigation.sessionIndexPage({ roomId, afterId, limit: 500 });
804
+ items.push(...page);
805
+ if (page.length < 500)
806
+ break;
807
+ afterId = page.at(-1).piboSessionId;
808
+ }
809
+ return items;
810
+ }
811
+ const sharedSessionSnapshots = new Map();
757
812
  function listSharedSessions(context) {
758
- const sessions = context.channelContext.listSessions?.() ?? context.channelContext.findSessions({});
813
+ const revision = context.channelContext.getSessionStructureRevision?.();
759
814
  const profiles = context.channelContext.getProfiles?.();
760
- return sessions
761
- .map((session) => canonicalizeSessionProfile(context, session, profiles))
762
- .sort(compareChatWebSessionsBySidebarOrder);
815
+ const profileKey = JSON.stringify(profiles?.map(profile => [profile.name, profile.aliases]) ?? []);
816
+ const cached = sharedSessionSnapshots.get(context.channelContext);
817
+ if (revision !== undefined && cached?.revision === revision && cached.profiles === profileKey)
818
+ return cached.sessions.slice();
819
+ const sessions = (context.channelContext.listSessions?.() ?? context.channelContext.findSessions({})).map(session => canonicalizeSessionProfile(context, session, profiles)).sort(compareChatWebSessionsBySidebarOrder);
820
+ if (revision !== undefined) {
821
+ sharedSessionSnapshots.delete(context.channelContext);
822
+ try {
823
+ let bytes = 16;
824
+ for (const session of sessions) {
825
+ bytes += boundedMessageBytes(session, 16 * 1024 * 1024 - bytes);
826
+ if (bytes > 16 * 1024 * 1024)
827
+ throw Error("Session snapshot budget exceeded");
828
+ }
829
+ sharedSessionSnapshots.set(context.channelContext, { revision: context.channelContext.getSessionStructureRevision?.() ?? revision, profiles: profileKey, sessions, bytes });
830
+ while (sharedSessionSnapshots.size > 4 || [...sharedSessionSnapshots.values()].reduce((total, item) => total + item.bytes, 0) > 16 * 1024 * 1024)
831
+ sharedSessionSnapshots.delete(sharedSessionSnapshots.keys().next().value);
832
+ }
833
+ catch { }
834
+ }
835
+ return sessions.slice();
763
836
  }
764
837
  function canonicalizeSessionProfile(context, session, profiles = context.channelContext.getProfiles?.()) {
765
838
  const canonicalProfile = canonicalProfileName(profiles, session.profile);
@@ -2798,23 +2871,29 @@ function sessionSubtree(sessions, rootSessionId) {
2798
2871
  }
2799
2872
  return [...subtree.values()];
2800
2873
  }
2801
- function buildSessionUnreadCounts(state, sessions) {
2874
+ async function buildSessionUnreadCounts(state, sessions) {
2802
2875
  const sessionsById = new Map(sessions.map((session) => [session.id, session]));
2803
2876
  const visibleSessionIds = sessions
2804
2877
  .filter((session) => !hasArchivedSessionInPath(session, sessionsById))
2805
2878
  .map((session) => session.id);
2879
+ if (state.readQueries)
2880
+ return new Map(await state.readQueries.navigation.unreadCountsPage({ piboSessionIds: visibleSessionIds }));
2806
2881
  return state.readState.countUnreadMessagesBySession({
2807
2882
  piboSessionIds: visibleSessionIds,
2808
2883
  });
2809
2884
  }
2810
- function hasUnreadInSessionSubtree(sessions, sessionUnreadCounts, rootSessionId) {
2811
- return sessionSubtree(sessions, rootSessionId).some((session) => (sessionUnreadCounts.get(session.id) ?? 0) > 0);
2885
+ function buildSessionUnreadErrors(state, sessions) {
2886
+ const sessionsById = new Map(sessions.map((session) => [session.id, session]));
2887
+ const visibleSessionIds = sessions
2888
+ .filter((session) => !hasArchivedSessionInPath(session, sessionsById))
2889
+ .map((session) => session.id);
2890
+ return state.readState.hasUnreadErrorsBySession({ piboSessionIds: visibleSessionIds });
2812
2891
  }
2813
- function sessionIdsWithUnreadInSubtree(sessions, sessionUnreadCounts) {
2892
+ function sessionIdsWithUnreadInSubtree(sessions, unreadSessionIds) {
2814
2893
  const sessionsById = new Map(sessions.map((session) => [session.id, session]));
2815
2894
  const result = new Set();
2816
2895
  for (const session of sessions) {
2817
- if ((sessionUnreadCounts.get(session.id) ?? 0) <= 0)
2896
+ if (!unreadSessionIds.has(session.id))
2818
2897
  continue;
2819
2898
  let current = session;
2820
2899
  const visited = new Set();
@@ -2827,11 +2906,7 @@ function sessionIdsWithUnreadInSubtree(sessions, sessionUnreadCounts) {
2827
2906
  return result;
2828
2907
  }
2829
2908
  function signalStatusHasUnreadError(options, piboSessionId) {
2830
- if (options.sessionIdsWithUnreadInSubtree)
2831
- return options.sessionIdsWithUnreadInSubtree.has(piboSessionId);
2832
- return options.sessions && options.sessionUnreadCounts
2833
- ? hasUnreadInSessionSubtree(options.sessions, options.sessionUnreadCounts, piboSessionId)
2834
- : true;
2909
+ return options.sessionIdsWithUnreadErrorInSubtree?.has(piboSessionId) ?? true;
2835
2910
  }
2836
2911
  function signalStatusFromSnapshot(snapshot, piboSessionId, options = {}) {
2837
2912
  const session = snapshot?.sessions[piboSessionId];
@@ -2853,19 +2928,19 @@ function signalStatusFromSummary(summary, piboSessionId, options = {}) {
2853
2928
  return { status: "error", updatedAt: summary.updatedAt };
2854
2929
  return { status: "idle", updatedAt: summary.updatedAt };
2855
2930
  }
2856
- function sessionIndexItemsWithSignalState(context, sessions, indexItems, sessionUnreadCounts = new Map()) {
2931
+ function sessionIndexItemsWithSignalState(context, sessions, indexItems, sessionUnreadErrors = new Set()) {
2857
2932
  const snapshotSignalStatuses = context.channelContext.snapshotSignalStatuses;
2858
2933
  const signalStatuses = snapshotSignalStatuses?.().sessions;
2859
2934
  const snapshotSignalSession = context.channelContext.snapshotSignalSession;
2860
2935
  if (!signalStatuses && !snapshotSignalSession)
2861
2936
  return [...indexItems];
2862
2937
  const bySessionId = new Map(indexItems.map((item) => [item.piboSessionId, item]));
2863
- const unreadSessionSubtreeIds = sessionIdsWithUnreadInSubtree(sessions, sessionUnreadCounts);
2938
+ const unreadErrorSubtreeIds = sessionIdsWithUnreadInSubtree(sessions, sessionUnreadErrors);
2864
2939
  for (const session of sessions) {
2865
2940
  const existing = bySessionId.get(session.id);
2866
2941
  const signal = signalStatuses
2867
- ? signalStatusFromSummary(signalStatuses[session.id], session.id, { sessionIdsWithUnreadInSubtree: unreadSessionSubtreeIds })
2868
- : signalStatusFromSnapshot(snapshotSignalSession?.(session.id), session.id, { sessionIdsWithUnreadInSubtree: unreadSessionSubtreeIds });
2942
+ ? signalStatusFromSummary(signalStatuses[session.id], session.id, { sessionIdsWithUnreadErrorInSubtree: unreadErrorSubtreeIds })
2943
+ : signalStatusFromSnapshot(snapshotSignalSession?.(session.id), session.id, { sessionIdsWithUnreadErrorInSubtree: unreadErrorSubtreeIds });
2869
2944
  if (!signal?.status)
2870
2945
  continue;
2871
2946
  if (signal.status === "idle" && existing?.status !== "running" && existing?.status !== "error")
@@ -2893,7 +2968,7 @@ function buildRoomUnreadCounts(sessions, sessionUnreadCounts, defaultRoomId) {
2893
2968
  const counts = new Map();
2894
2969
  const sessionsById = new Map(sessions.map((session) => [session.id, session]));
2895
2970
  for (const session of sessions) {
2896
- if (hasArchivedSessionInPath(session, sessionsById))
2971
+ if (session.parentId || hasArchivedSessionInPath(session, sessionsById))
2897
2972
  continue;
2898
2973
  const unreadCount = sessionUnreadCounts.get(session.id) ?? 0;
2899
2974
  if (unreadCount <= 0)
@@ -3141,10 +3216,30 @@ function writeChatEventFrames(controller, event, state, cursor, options = { mode
3141
3216
  return;
3142
3217
  if (options.mode === "summary" && isLiveOnlyOutputEvent(event.payload))
3143
3218
  return;
3219
+ if (isLiveOnlyOutputEvent(event.payload)) {
3220
+ try {
3221
+ boundedMessageBytes(event.payload, 1024 * 1024);
3222
+ }
3223
+ catch {
3224
+ return;
3225
+ }
3226
+ }
3144
3227
  const piboSessionId = event.piboSessionId ?? event.payload.piboSessionId;
3145
3228
  const streamId = "streamId" in event ? event.streamId : undefined;
3146
3229
  const createdAt = chatLiveEventCreatedAt(event);
3147
- const frames = chatStreamFramesFromOutputEvent(event.payload, state, {
3230
+ const ref = "storedPayloadRef" in event ? event.storedPayloadRef : undefined;
3231
+ let payload = event.payload;
3232
+ if (ref && ref.byteLength > 64 * 1024) {
3233
+ if (payload.type === "assistant_message" || payload.type === "thinking_finished")
3234
+ payload = { ...payload, text: ref.preview };
3235
+ else if (payload.type === "tool_execution_finished")
3236
+ payload = { ...payload, result: null };
3237
+ else if (payload.type === "tool_execution_updated")
3238
+ payload = { ...payload, partialResult: null };
3239
+ else if (payload.type === "tool_call" || payload.type === "tool_execution_started")
3240
+ payload = { ...payload, args: {} };
3241
+ }
3242
+ const frames = chatStreamFramesFromOutputEvent(payload, state, {
3148
3243
  includeRawEvent: streamId !== undefined && isPersistableOutputEvent(event.payload),
3149
3244
  });
3150
3245
  for (let index = 0; index < frames.length; index += 1) {
@@ -3153,6 +3248,7 @@ function writeChatEventFrames(controller, event, state, cursor, options = { mode
3153
3248
  const frameId = streamId === undefined ? nextTransientChatStreamFrameId(state) : `${streamId}:${index}`;
3154
3249
  writeSse(controller, "pibo", {
3155
3250
  ...frames[index],
3251
+ ...("storedPayloadRef" in event && event.storedPayloadRef ? { storedPayloadRef: event.storedPayloadRef } : {}),
3156
3252
  piboSessionId,
3157
3253
  ...(createdAt ? { createdAt } : {}),
3158
3254
  ...(!("streamId" in event) && event.replaySequence !== undefined ? { liveReplayId: event.replaySequence } : {}),
@@ -3167,81 +3263,124 @@ function chatLiveEventCreatedAt(event) {
3167
3263
  }
3168
3264
  return undefined;
3169
3265
  }
3266
+ function trackedEventStream(state, onClose) {
3267
+ const stream = new BoundedEventStream(reason => { state.boundedStreams.delete(stream); if (reason.startsWith("slow") || reason === "error")
3268
+ state.boundedStreamDisconnects++; onClose(); });
3269
+ state.boundedStreams.add(stream);
3270
+ return stream;
3271
+ }
3170
3272
  function createEventStream(input) {
3171
3273
  let unsubscribe;
3172
3274
  let heartbeat;
3173
3275
  let registeredLiveObserver = false;
3174
3276
  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;
3277
+ const stream = trackedEventStream(input.state, () => {
3278
+ unsubscribe?.();
3279
+ unsubscribe = undefined;
3280
+ if (heartbeat)
3281
+ clearInterval(heartbeat);
3282
+ heartbeat = undefined;
3283
+ if (registeredLiveObserver && input.activePiboSessionId) {
3284
+ markEventStreamDisconnected({ state: input.state, piboSessionId: input.activePiboSessionId, streamId });
3285
+ registeredLiveObserver = false;
3286
+ }
3287
+ });
3288
+ const controller = stream.writer;
3289
+ const streamState = createChatStreamState();
3290
+ let replaying = true;
3291
+ let lastReplayedStreamId = -1;
3292
+ const pendingLive = [];
3293
+ let pendingLiveBytes = 0;
3294
+ const listener = (event) => {
3295
+ if (!liveEventMatches(event, input) || stream.closed)
3296
+ return;
3297
+ if ("streamId" in event && typeof event.streamId === "number" && event.streamId <= lastReplayedStreamId)
3298
+ return;
3299
+ if (replaying) {
3300
+ try {
3301
+ pendingLiveBytes += boundedMessageBytes(event, 1024 * 1024);
3302
+ if (pendingLive.length >= 128 || pendingLiveBytes > 1024 * 1024)
3303
+ throw Error("Replay race buffer full");
3304
+ pendingLive.push(event);
3180
3305
  }
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 });
3306
+ catch {
3307
+ stream.fail();
3199
3308
  }
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 });
3309
+ }
3310
+ else
3311
+ writeChatEventFrames(controller, event, streamState, undefined, { mode: input.mode });
3312
+ };
3313
+ input.state.liveListeners.add(listener);
3314
+ unsubscribe = () => input.state.liveListeners.delete(listener);
3315
+ if (input.mode === "live" && input.activePiboSessionId) {
3316
+ markEventStreamConnected(input.state, input.activePiboSessionId, streamId);
3317
+ registeredLiveObserver = true;
3318
+ }
3319
+ const initialSnapshots = input.mode === "live" && input.piboSessionId ? input.state.outputCompactor.snapshotsForSession(input.piboSessionId) : [];
3320
+ void (async () => {
3321
+ const transientReplay = input.mode === "live" ? collectTransientReplayEvents(input.state, { roomId: input.roomId, piboSessionId: input.piboSessionId, afterReplaySequence: input.transientReplayCursor }) : undefined;
3322
+ writeSse(controller, "pibo", { type: "ready", piboSessionId: input.piboSessionId ?? "", ...(transientReplay?.status ? { liveReplay: transientReplay.status } : {}) });
3323
+ let afterStreamId = input.cursor ? Math.max(0, input.cursor.streamId - 1) : undefined;
3324
+ let replayed = 0;
3325
+ let pageSize = 16;
3326
+ while (!stream.closed) {
3327
+ if (!await stream.waitForCapacity())
3328
+ return;
3329
+ let events;
3330
+ try {
3331
+ events = await (input.state.readQueries?.timeline ?? input.state.timelineQuery).listEvents({ roomId: input.roomId, piboSessionId: input.piboSessionId, afterStreamId, limit: pageSize });
3332
+ }
3333
+ catch (error) {
3334
+ if (pageSize > 1 && error && typeof error === "object" && "code" in error && error.code === "storage_payload_limit") {
3335
+ pageSize = Math.max(1, Math.floor(pageSize / 2));
3336
+ continue;
3208
3337
  }
3338
+ throw error;
3209
3339
  }
3210
- const listener = (event) => {
3211
- if (!liveEventMatches(event, input))
3340
+ for (const stored of events) {
3341
+ if (!await stream.waitForCapacity())
3212
3342
  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;
3343
+ writeChatEventFrames(controller, stored, streamState, input.cursor, { mode: input.mode });
3344
+ afterStreamId = stored.streamId;
3345
+ lastReplayedStreamId = stored.streamId;
3346
+ replayed++;
3234
3347
  }
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
- });
3348
+ if (events.length < pageSize)
3349
+ break;
3350
+ if (replayed >= 1000) {
3351
+ stream.finish();
3352
+ return;
3353
+ }
3354
+ }
3355
+ if (stream.closed)
3356
+ return;
3357
+ if (input.mode === "live" && input.piboSessionId) {
3358
+ if (input.transientReplayCursor === undefined)
3359
+ for (const snapshot of initialSnapshots) {
3360
+ if (!await stream.waitForCapacity())
3361
+ return;
3362
+ writeChatEventFrames(controller, { piboSessionId: snapshot.piboSessionId, eventType: snapshot.type, payload: snapshot }, streamState, undefined, { mode: input.mode });
3363
+ }
3364
+ for (const replay of transientReplay?.events ?? []) {
3365
+ if (!await stream.waitForCapacity())
3366
+ return;
3367
+ writeChatEventFrames(controller, replay, streamState, undefined, { mode: input.mode });
3368
+ }
3369
+ }
3370
+ for (const event of pendingLive) {
3371
+ if ("streamId" in event && typeof event.streamId === "number" && event.streamId <= lastReplayedStreamId)
3372
+ continue;
3373
+ if (!await stream.waitForCapacity())
3374
+ return;
3375
+ writeChatEventFrames(controller, event, streamState, undefined, { mode: input.mode });
3376
+ }
3377
+ pendingLive.length = 0;
3378
+ pendingLiveBytes = 0;
3379
+ replaying = false;
3380
+ if (!stream.closed)
3381
+ heartbeat = setInterval(() => writeSseComment(controller, "heartbeat"), 25000);
3382
+ })().catch(() => stream.fail());
3383
+ 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
3384
  }
3246
3385
  function enrichWorkflowSession(state, workflowSession) {
3247
3386
  const snapshot = state.workflowService.getWorkflowSessionSnapshotForSession(workflowSession.piboSessionId);
@@ -3422,116 +3561,201 @@ function startChatStreamingFixture(input) {
3422
3561
  },
3423
3562
  });
3424
3563
  }
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
- }
3564
+ async function resolveAdmissionSession(storage, context, webSession, defaultProfile, piboSessionId, requestedRoomId) {
3565
+ if (piboSessionId) {
3566
+ const found = context.channelContext.getSession(piboSessionId);
3567
+ if (!found)
3568
+ throw new PiboWebHttpError("Session not found", 404);
3569
+ const session = canonicalizeSessionProfile(context, found);
3570
+ const roomId = chatRoomIdFromMetadata(session.metadata);
3571
+ const room = await storage.resolveRoom(roomId);
3572
+ if (requestedRoomId && requestedRoomId !== room.id)
3573
+ throw new PiboWebHttpError("Session is not available in this room", 404);
3574
+ if (!roomId)
3575
+ context.channelContext.updateSession?.(session.id, { metadata: withChatRoomId(session.metadata, room.id) });
3576
+ return { session: { ...session, metadata: withChatRoomId(session.metadata, room.id) }, room };
3577
+ }
3578
+ const room = await storage.resolveRoom(requestedRoomId, Boolean(requestedRoomId));
3579
+ const candidates = listSharedSessions(context);
3580
+ const existing = candidates.find(session => !session.parentId && !isChatWebSessionArchived(session) && chatRoomIdFromMetadata(session.metadata) === room.id);
3581
+ if (existing)
3582
+ return { session: existing, room };
3435
3583
  if (isPiboRoomArchived(room)) {
3436
- throw new PiboWebHttpError("Archived rooms are read-only", 403);
3584
+ const archived = candidates.find(session => !session.parentId && chatRoomIdFromMetadata(session.metadata) === room.id);
3585
+ if (archived)
3586
+ return { session: archived, room };
3587
+ throw new PiboWebHttpError("Archived room has no sessions", 404);
3437
3588
  }
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
- });
3589
+ return { session: createSharedChatSession(context, webSession, defaultProfile, room), room };
3590
+ }
3591
+ async function sendChatMessage(input) {
3479
3592
  try {
3480
- input.state.ingestService?.ingestUserMessageAccepted({
3481
- session: selectedSession,
3482
- roomId: room.id,
3483
- actorId,
3484
- text: fileAttachmentContext.messageText,
3485
- clientTxnId,
3486
- legacyEvent: accepted,
3593
+ const startedAt = performance.now();
3594
+ const timings = [];
3595
+ const timedResponse = (value, status = 200) => responseJson(value, {
3596
+ status,
3597
+ headers: { "server-timing": [...timings, `chat_ack;dur=${(performance.now() - startedAt).toFixed(2)}`].join(", ") },
3487
3598
  });
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",
3599
+ const durable = input.body.admissionVersion === 2;
3600
+ if (input.body.admissionVersion !== undefined && !durable)
3601
+ throw new PiboWebHttpError("Unsupported message admission version", 400);
3602
+ if (durable && !input.state.asyncStorage)
3603
+ throw new PiboWebHttpError("Durable admission requires file-backed storage", 503);
3604
+ const text = normalizeMessageText(input.body.text);
3605
+ const delivery = normalizeMessageDelivery(input.body.delivery);
3606
+ const clientTxnId = normalizeClientTxnId(input.body.clientTxnId);
3607
+ const requestedRoomId = input.forcedRoomId ?? (typeof input.body.roomId === "string" ? input.body.roomId : undefined);
3608
+ const requestedSessionId = typeof input.body.piboSessionId === "string" ? input.body.piboSessionId : undefined;
3609
+ const resolved = input.state.asyncStorage
3610
+ ? await resolveAdmissionSession(input.state.asyncStorage, input.context, input.webSession, input.defaultProfile, requestedSessionId, requestedRoomId)
3611
+ : undefined;
3612
+ const selectedSession = resolved?.session ?? resolveRequestedSession(input.state, input.context, input.webSession, input.defaultProfile, requestedSessionId, requestedRoomId);
3613
+ const room = resolved?.room ?? ensureSessionRoom(input.state, input.context, selectedSession, input.webSession);
3614
+ if (requestedRoomId && room.id !== requestedRoomId) {
3615
+ throw new PiboWebHttpError("Session is not available in this room", 404);
3616
+ }
3617
+ if (isPiboRoomArchived(room)) {
3618
+ throw new PiboWebHttpError("Archived rooms are read-only", 403);
3619
+ }
3620
+ if (!input.state.asyncStorage)
3621
+ input.state.sessionQuery.upsertSession(selectedSession);
3622
+ const actorId = auditActorIdFor(input.webSession);
3623
+ const lookupStartedAt = performance.now();
3624
+ const duplicate = clientTxnId && !input.state.asyncStorage ? input.state.eventCommands.findByClientTxn(room.id, actorId, clientTxnId) : undefined;
3625
+ timings.push(`chat_lookup;dur=${(performance.now() - lookupStartedAt).toFixed(2)}`);
3626
+ if (duplicate)
3627
+ return timedResponse({ duplicate: true, event: duplicate });
3628
+ const webAnnotationContext = prepareWebAnnotationAttachments({
3499
3629
  piboSessionId: selectedSession.id,
3500
- id: messageId,
3501
- text: fileAttachmentContext.messageText,
3502
- delivery,
3503
- source: "user",
3630
+ messageText: text,
3631
+ attachmentIds: input.body.webAnnotationIds,
3504
3632
  });
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({
3633
+ const fileAttachmentContext = prepareChatFileAttachments({
3634
+ messageText: webAnnotationContext.messageText,
3635
+ attachmentPaths: input.body.fileAttachmentPaths,
3636
+ });
3637
+ const appendStartedAt = performance.now();
3638
+ const appendInput = {
3512
3639
  roomId: room.id,
3513
3640
  piboSessionId: selectedSession.id,
3514
- eventType: "user.message.failed",
3515
- actorType: "system",
3641
+ eventType: "user.message.accepted",
3642
+ actorType: "user",
3516
3643
  actorId,
3517
- retentionClass: "audit_event",
3644
+ clientTxnId,
3645
+ retentionClass: "chat_message",
3518
3646
  payload: {
3519
- type: "user.message.failed",
3647
+ type: "user.message.accepted",
3520
3648
  piboSessionId: selectedSession.id,
3521
3649
  roomId: room.id,
3650
+ text: fileAttachmentContext.messageText,
3651
+ delivery,
3652
+ ...(webAnnotationContext.attachments.length ? {
3653
+ webAnnotationIds: webAnnotationContext.ids,
3654
+ webAnnotationAttachments: webAnnotationContext.attachments,
3655
+ webAnnotationContext: webAnnotationContext.modelContext,
3656
+ } : {}),
3657
+ ...(fileAttachmentContext.attachments.length ? {
3658
+ fileAttachmentPaths: fileAttachmentContext.paths,
3659
+ fileAttachments: fileAttachmentContext.attachments,
3660
+ fileAttachmentContext: fileAttachmentContext.modelContext,
3661
+ } : {}),
3522
3662
  ...(clientTxnId ? { clientTxnId } : {}),
3523
- message: errorMessage,
3524
3663
  },
3525
- });
3664
+ };
3665
+ const messageId = clientTxnId ?? randomUUID();
3666
+ const admission = input.state.asyncStorage ? await input.state.asyncStorage.admit(appendInput, selectedSession, fileAttachmentContext.messageText, durable ? { eventId: messageId, delivery } : undefined) : undefined;
3667
+ const accepted = admission?.event ?? input.state.eventCommands.appendEvent(appendInput);
3668
+ if (admission && !admission.created)
3669
+ 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);
3670
+ timings.push(`chat_append;dur=${(performance.now() - appendStartedAt).toFixed(2)}`);
3671
+ const ingestStartedAt = performance.now();
3672
+ try {
3673
+ if (!input.state.asyncStorage)
3674
+ input.state.ingestService?.ingestUserMessageAccepted({
3675
+ session: selectedSession,
3676
+ roomId: room.id,
3677
+ actorId,
3678
+ text: fileAttachmentContext.messageText,
3679
+ clientTxnId,
3680
+ legacyEvent: accepted,
3681
+ });
3682
+ }
3683
+ catch (error) {
3684
+ console.warn("V2 chat data shadow ingest failed", error);
3685
+ }
3686
+ timings.push(`chat_ingest;dur=${(performance.now() - ingestStartedAt).toFixed(2)}`);
3526
3687
  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);
3688
+ listener(accepted);
3689
+ if (durable && admission?.receipt) {
3690
+ input.state.commandDispatcher ??= new MessageCommandDispatcher(input.state.asyncStorage, input.context.channelContext);
3691
+ input.state.commandDispatcher.wake();
3692
+ markWebAnnotationsAttached(webAnnotationContext);
3693
+ return timedResponse({ admissionVersion: 2, receipt: admission.receipt, event: accepted, statusPath: `${CHAT_WEB_API_PREFIX}/message-receipts/${admission.receipt.id}` }, 202);
3694
+ }
3695
+ const emitStartedAt = performance.now();
3696
+ let output;
3697
+ try {
3698
+ output = await input.context.channelContext.emit({
3699
+ type: "message",
3700
+ piboSessionId: selectedSession.id,
3701
+ id: messageId,
3702
+ text: fileAttachmentContext.messageText,
3703
+ delivery,
3704
+ source: "user",
3705
+ });
3706
+ }
3707
+ catch (error) {
3708
+ const errorMessage = error instanceof Error ? error.message : String(error);
3709
+ if (!(error instanceof PiboSteeringUnavailableError)) {
3710
+ input.context.channelContext.reportSessionError?.(selectedSession.id, errorMessage, { eventId: messageId, source: "pibo" });
3711
+ }
3712
+ const failedInput = {
3713
+ roomId: room.id,
3714
+ piboSessionId: selectedSession.id,
3715
+ eventType: "user.message.failed",
3716
+ actorType: "system",
3717
+ actorId,
3718
+ retentionClass: "audit_event",
3719
+ payload: {
3720
+ type: "user.message.failed",
3721
+ piboSessionId: selectedSession.id,
3722
+ roomId: room.id,
3723
+ ...(clientTxnId ? { clientTxnId } : {}),
3724
+ message: errorMessage,
3725
+ },
3726
+ };
3727
+ const failed = input.state.asyncStorage ? (await input.state.asyncStorage.append(failedInput)).event : input.state.eventCommands.appendEvent(failedInput);
3728
+ for (const listener of input.state.liveListeners)
3729
+ listener(failed);
3730
+ if (error instanceof PiboSteeringUnavailableError || error instanceof AgentRuntimeBindingMissingError) {
3731
+ throw new PiboWebHttpError(error.message, 409);
3732
+ }
3733
+ throw error;
3734
+ }
3735
+ timings.push(`chat_emit;dur=${(performance.now() - emitStartedAt).toFixed(2)}`);
3736
+ markWebAnnotationsAttached(webAnnotationContext);
3737
+ return timedResponse({ output, event: accepted });
3738
+ }
3739
+ catch (error) {
3740
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
3741
+ if (code === "command_conflict")
3742
+ return responseJson({ error: "Transaction conflicts with an existing message.", code }, { status: 409 });
3743
+ if (code === "command_too_large")
3744
+ return responseJson({ error: "Message exceeds the durable command limit.", code }, { status: 413 });
3745
+ if (code === "command_reconciliation_required") {
3746
+ const details = error;
3747
+ return responseJson({ error: "A previous interrupted message requires review before this session can accept more messages.", code, retryable: false, scope: details.scope ?? "session", blockingCommandId: details.blockingCommandId, blockedSince: details.blockedSince, oldestWaitAgeMs: details.oldestWaitAgeMs, nextAction: details.nextAction }, { status: 409 });
3530
3748
  }
3749
+ if (code === "command_overloaded")
3750
+ return responseJson({ error: "Message queue capacity reached.", code, retryable: true, scope: "capacity" }, { status: 429, headers: { "retry-after": "1" } });
3751
+ if (code === "room_not_found")
3752
+ throw new PiboWebHttpError("Room not found", 404);
3753
+ if (code === "room_read_only")
3754
+ throw new PiboWebHttpError("Archived rooms are read-only", 403);
3755
+ if (code.startsWith("storage_"))
3756
+ 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
3757
  throw error;
3532
3758
  }
3533
- markWebAnnotationsAttached(webAnnotationContext);
3534
- return responseJson({ output, event: accepted });
3535
3759
  }
3536
3760
  export function createChatWebApp(options = {}) {
3537
3761
  const integrations = resolveChatWebIntegrations(options);
@@ -3550,6 +3774,9 @@ export function createChatWebApp(options = {}) {
3550
3774
  const state = {
3551
3775
  sessionQuery: new ChatSessionQueryService(dataStore),
3552
3776
  timelineQuery: new ChatTimelineQueryService(dataStore),
3777
+ boundedStreams: new Set(),
3778
+ boundedStreamDisconnects: 0,
3779
+ readQueries: dataStore.path === ":memory:" ? undefined : new AsyncChatReadQueries(dataStore.path, options.dataPayloadRootDir ?? piboHomePath("payloads")),
3553
3780
  historyQuery: new ChatHistoryQueryService(dataStore),
3554
3781
  eventCommands: new ChatEventCommandService(dataStore),
3555
3782
  readState: new ChatReadStateService(dataStore),
@@ -3560,6 +3787,7 @@ export function createChatWebApp(options = {}) {
3560
3787
  cronStore: createDefaultPiboCronStore({ path: options.cronStorePath }),
3561
3788
  loopStore: createDefaultPiboLoopStore({ path: options.ralphStorePath }),
3562
3789
  dataStore,
3790
+ asyncStorage: dataStore.path === ":memory:" ? undefined : new AsyncChatStorage(dataStore.path, options.dataPayloadRootDir ?? piboHomePath("payloads")),
3563
3791
  ingestService: new ChatDataIngestService(dataStore),
3564
3792
  traceCache: new Map(),
3565
3793
  traceTimelinePageCache: new Map(),
@@ -3589,30 +3817,59 @@ export function createChatWebApp(options = {}) {
3589
3817
  telemetryRetentionMaintenance: {},
3590
3818
  integrations,
3591
3819
  };
3820
+ const earlyTraceCache = new TraceResponseCache();
3592
3821
  let disposed = false;
3593
3822
  const requireSession = (request, context) => context.requireSession({
3594
3823
  request,
3595
3824
  });
3596
- return {
3825
+ const application = {
3597
3826
  name: CHAT_WEB_APP_NAME,
3598
3827
  mountPath: CHAT_WEB_MOUNT_PATH,
3599
3828
  apiPrefix: CHAT_WEB_API_PREFIX,
3600
- dispose() {
3829
+ initialize(context) {
3830
+ ensureCustomAgentProfiles(state, context);
3831
+ ensureEventIndexing(state, context);
3832
+ if (state.asyncStorage)
3833
+ state.commandDispatcher ??= new MessageCommandDispatcher(state.asyncStorage, context.channelContext);
3834
+ },
3835
+ async gatewayStatus() {
3836
+ if (!state.asyncStorage)
3837
+ return { durableMessageQueue: { status: "ambiguous", storage: { available: false, error: "Durable message storage is not file-backed." }, degradedReasons: ["durable message storage unavailable"] } };
3838
+ try {
3839
+ return { durableMessageQueue: await state.asyncStorage.durableQueueHealth() };
3840
+ }
3841
+ catch (error) {
3842
+ return { durableMessageQueue: { status: "ambiguous", storage: { available: false, error: error instanceof Error ? error.message : "Storage unavailable" }, degradedReasons: ["durable message queue storage read failed"] } };
3843
+ }
3844
+ },
3845
+ async drain() {
3846
+ await state.outputPersistenceRetries.drain();
3847
+ },
3848
+ async dispose() {
3601
3849
  if (disposed)
3602
3850
  return;
3603
3851
  disposed = true;
3852
+ earlyTraceCache.clear();
3604
3853
  state.unsubscribe?.();
3605
3854
  state.unsubscribe = undefined;
3855
+ if (state.subscribedContext)
3856
+ sharedSessionSnapshots.delete(state.subscribedContext.channelContext);
3606
3857
  state.subscribedContext = undefined;
3607
3858
  state.eventLoopDelay.disable();
3859
+ await state.commandDispatcher?.dispose();
3860
+ await disposeTelemetryRetentionMaintenance(state.telemetryRetentionMaintenance);
3608
3861
  state.outputPersistenceRetries.dispose();
3609
3862
  state.workflowService.close();
3610
3863
  state.agentStore.close();
3611
3864
  state.reliabilityStore.close();
3612
3865
  state.cronStore.close();
3613
3866
  state.loopStore.close();
3867
+ for (const stream of state.boundedStreams)
3868
+ stream.fail();
3614
3869
  state.outputCompactor.disposeAll();
3615
3870
  state.outputRenderSequencer.disposeAll();
3871
+ await state.asyncStorage?.close();
3872
+ await state.readQueries?.close();
3616
3873
  state.dataStore.close();
3617
3874
  },
3618
3875
  async handleRequest(request, context) {
@@ -3678,7 +3935,7 @@ export function createChatWebApp(options = {}) {
3678
3935
  if (!nodeId || parsed.nodeId !== nodeId || parsed.payloadKind !== "output")
3679
3936
  throw new PiboWebHttpError("Trace image node does not match the payload ref", 400);
3680
3937
  resolveRequestedSession(state, context, webSession, defaultProfile, parsed.piboSessionId);
3681
- if (!state.timelineQuery.isPayloadAttachedToTraceNode({
3938
+ if (!await (state.readQueries?.timeline ?? state.timelineQuery).isPayloadAttachedToTraceNode({
3682
3939
  piboSessionId: parsed.piboSessionId,
3683
3940
  payloadId: parsed.payloadId,
3684
3941
  nodeId,
@@ -3758,6 +4015,7 @@ export function createChatWebApp(options = {}) {
3758
4015
  const requestedRoomId = url.searchParams.get("roomId") || undefined;
3759
4016
  const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined, requestedRoomId);
3760
4017
  const selectedRoomId = selectedRoomIdForSession(state, context, selectedSession);
4018
+ const structuralRevision = context.channelContext.getSessionStructureRevision?.();
3761
4019
  const ownedSessions = listSharedSessions(context);
3762
4020
  const roomSessions = visibleSessionsInRoom({
3763
4021
  state,
@@ -3769,9 +4027,15 @@ export function createChatWebApp(options = {}) {
3769
4027
  includeArchived,
3770
4028
  });
3771
4029
  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 });
4030
+ const indexKey = JSON.stringify([structuralRevision, selectedRoomId, includeArchived, selectedSession.id]);
4031
+ if (structuralRevision === undefined || state.navigationIndexed?.context !== context.channelContext || state.navigationIndexed.key !== indexKey) {
4032
+ indexSharedSessions(state.sessionQuery, roomSessions);
4033
+ if (structuralRevision !== undefined)
4034
+ state.navigationIndexed = { context: context.channelContext, key: indexKey };
4035
+ }
4036
+ const sessionUnreadCounts = await buildSessionUnreadCounts(state, ownedSessions);
4037
+ const sessionUnreadErrors = buildSessionUnreadErrors(state, ownedSessions);
4038
+ const sessions = await buildSessionNodes(roomSessions, sessionIndexItemsWithSignalState(context, roomSessions, await readNavigationIndex(state, selectedRoomId), sessionUnreadErrors), process.cwd(), sessionUnreadCounts, { skipPiMetadataFallback: true });
3775
4039
  const roomTree = state.roomService.listRoomTree();
3776
4040
  const roomUnreadCounts = buildRoomUnreadCounts(ownedSessions, sessionUnreadCounts, defaultRoom.id);
3777
4041
  const rooms = roomsWithUnreadCounts(roomTree, roomUnreadCounts);
@@ -3783,7 +4047,7 @@ export function createChatWebApp(options = {}) {
3783
4047
  defaultRoomId: defaultRoom.id,
3784
4048
  selectedRoomId,
3785
4049
  selectedPiboSessionId: selectedSession.id,
3786
- latestRoomStreamId: state.timelineQuery.getLatestStreamId({ roomId: selectedRoomId }),
4050
+ latestRoomStreamId: await (state.readQueries?.timeline ?? state.timelineQuery).getLatestStreamId({ roomId: selectedRoomId }),
3787
4051
  rooms,
3788
4052
  sessions,
3789
4053
  }, { headers: { "server-timing": "navigation;desc=\"no_catalog_no_jsonl\"" } });
@@ -3795,6 +4059,7 @@ export function createChatWebApp(options = {}) {
3795
4059
  const requestedRoomId = url.searchParams.get("roomId") || undefined;
3796
4060
  const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined, requestedRoomId);
3797
4061
  const selectedRoomId = selectedRoomIdForSession(state, context, selectedSession);
4062
+ const structuralRevision = context.channelContext.getSessionStructureRevision?.();
3798
4063
  const ownedSessions = listSharedSessions(context);
3799
4064
  const roomSessions = visibleSessionsInRoom({
3800
4065
  state,
@@ -3809,10 +4074,16 @@ export function createChatWebApp(options = {}) {
3809
4074
  if (markRead) {
3810
4075
  markSessionsRead(state, sessionSubtree(ownedSessions, selectedSession.id));
3811
4076
  }
3812
- indexSharedSessions(state.sessionQuery, roomSessions);
3813
- const sessionUnreadCounts = buildSessionUnreadCounts(state, ownedSessions);
4077
+ const indexKey = JSON.stringify([structuralRevision, selectedRoomId, includeArchived, selectedSession.id]);
4078
+ if (structuralRevision === undefined || state.navigationIndexed?.context !== context.channelContext || state.navigationIndexed.key !== indexKey) {
4079
+ indexSharedSessions(state.sessionQuery, roomSessions);
4080
+ if (structuralRevision !== undefined)
4081
+ state.navigationIndexed = { context: context.channelContext, key: indexKey };
4082
+ }
4083
+ const sessionUnreadCounts = await buildSessionUnreadCounts(state, ownedSessions);
4084
+ const sessionUnreadErrors = buildSessionUnreadErrors(state, ownedSessions);
3814
4085
  const [sessions, catalog] = await Promise.all([
3815
- buildSessionNodes(roomSessions, sessionIndexItemsWithSignalState(context, roomSessions, state.sessionQuery.listSessions(), sessionUnreadCounts), process.cwd(), sessionUnreadCounts, sessionNodeHistoryOptions(context)),
4086
+ buildSessionNodes(roomSessions, sessionIndexItemsWithSignalState(context, roomSessions, await readNavigationIndex(state, selectedRoomId), sessionUnreadErrors), process.cwd(), sessionUnreadCounts, sessionNodeHistoryOptions(context)),
3816
4087
  loadBootstrapCatalog(state, context, webSession),
3817
4088
  ]);
3818
4089
  const roomTree = state.roomService.listRoomTree();
@@ -3825,7 +4096,7 @@ export function createChatWebApp(options = {}) {
3825
4096
  room: state.roomService.getRoom(selectedRoomId),
3826
4097
  selectedRoomId,
3827
4098
  selectedPiboSessionId: selectedSession.id,
3828
- latestRoomStreamId: state.timelineQuery.getLatestStreamId({ roomId: selectedRoomId }),
4099
+ latestRoomStreamId: await (state.readQueries?.timeline ?? state.timelineQuery).getLatestStreamId({ roomId: selectedRoomId }),
3829
4100
  rooms,
3830
4101
  sessions,
3831
4102
  ...catalog,
@@ -3943,29 +4214,32 @@ export function createChatWebApp(options = {}) {
3943
4214
  let unsubscribe;
3944
4215
  let heartbeat;
3945
4216
  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
- });
4217
+ const cleanup = () => {
4218
+ closed = true;
4219
+ unsubscribe?.();
4220
+ unsubscribe = undefined;
4221
+ if (heartbeat)
4222
+ clearInterval(heartbeat);
4223
+ heartbeat = undefined;
4224
+ };
4225
+ const bounded = trackedEventStream(state, cleanup);
4226
+ {
4227
+ const controller = bounded.writer;
4228
+ writeJsonSse(controller, "signal_status_snapshot", context.channelContext.snapshotSignalStatuses());
4229
+ unsubscribe = context.channelContext.subscribeSignalStatuses((patch) => {
4230
+ if (closed)
4231
+ return;
4232
+ const statusPatch = compactSignalStatusPatch(patch);
4233
+ writeJsonSse(controller, "signal_status_patch", statusPatch, `${patch.rootPiboSessionId}:${patch.toVersion}`);
4234
+ });
4235
+ heartbeat = setInterval(() => {
4236
+ if (!closed)
4237
+ writeSseComment(controller, "heartbeat");
4238
+ }, 25_000);
4239
+ }
4240
+ if (closed)
4241
+ cleanup();
4242
+ const stream = bounded.stream;
3969
4243
  return new Response(stream, {
3970
4244
  headers: signalSseHeaders(),
3971
4245
  });
@@ -3997,38 +4271,41 @@ export function createChatWebApp(options = {}) {
3997
4271
  let unsubscribeStatuses;
3998
4272
  let heartbeat;
3999
4273
  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));
4274
+ const cleanup = () => {
4275
+ closed = true;
4276
+ unsubscribeTree?.();
4277
+ unsubscribeTree = undefined;
4278
+ unsubscribeStatuses?.();
4279
+ unsubscribeStatuses = undefined;
4280
+ if (heartbeat)
4281
+ clearInterval(heartbeat);
4282
+ heartbeat = undefined;
4283
+ };
4284
+ const bounded = trackedEventStream(state, cleanup);
4285
+ {
4286
+ const controller = bounded.writer;
4287
+ writeJsonSse(controller, "signal_snapshot", context.channelContext.snapshotSignalTree(rootPiboSessionId));
4288
+ unsubscribeTree = context.channelContext.subscribeSignalTree(rootPiboSessionId, (patch) => {
4289
+ if (!closed)
4290
+ writeJsonSse(controller, "signal_patch", patch, String(patch.toVersion));
4291
+ });
4292
+ if (includeStatuses) {
4293
+ writeJsonSse(controller, "signal_status_snapshot", context.channelContext.snapshotSignalStatuses());
4294
+ unsubscribeStatuses = context.channelContext.subscribeSignalStatuses((patch) => {
4295
+ if (closed)
4296
+ return;
4297
+ const statusPatch = compactSignalStatusPatch(patch);
4298
+ writeJsonSse(controller, "signal_status_patch", statusPatch, `${patch.rootPiboSessionId}:${patch.toVersion}`);
4006
4299
  });
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
- });
4300
+ }
4301
+ heartbeat = setInterval(() => {
4302
+ if (!closed)
4303
+ writeSseComment(controller, "heartbeat");
4304
+ }, 25_000);
4305
+ }
4306
+ if (closed)
4307
+ cleanup();
4308
+ const stream = bounded.stream;
4032
4309
  return new Response(stream, {
4033
4310
  headers: signalSseHeaders(),
4034
4311
  });
@@ -4653,13 +4930,22 @@ export function createChatWebApp(options = {}) {
4653
4930
  const webSession = await requireSession(request, context);
4654
4931
  requireRoom(state, roomResource.roomId, webSession, "read");
4655
4932
  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
- });
4933
+ let limit = 1000;
4934
+ for (;;) {
4935
+ try {
4936
+ const events = await (state.readQueries?.timeline ?? state.timelineQuery).listEvents({
4937
+ roomId: roomResource.roomId,
4938
+ afterStreamId: cursor?.streamId,
4939
+ limit,
4940
+ });
4941
+ return responseJson({ events });
4942
+ }
4943
+ catch (error) {
4944
+ if (limit <= 1 || !error || typeof error !== "object" || !("code" in error) || error.code !== "storage_payload_limit")
4945
+ throw error;
4946
+ limit = Math.max(1, Math.floor(limit / 2));
4947
+ }
4948
+ }
4663
4949
  }
4664
4950
  if (roomResource && roomResource.child === "messages" && request.method === "POST") {
4665
4951
  requireSameOriginJsonRequest(request);
@@ -4861,16 +5147,16 @@ export function createChatWebApp(options = {}) {
4861
5147
  const startedAt = performance.now();
4862
5148
  const webSession = await requireSession(request, context);
4863
5149
  const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined);
4864
- state.sessionQuery.upsertSession(selectedSession);
5150
+ state.sessionQuery.upsertSessionsIfChanged([selectedSession]);
4865
5151
  const indexedSession = state.sessionQuery.getSession(selectedSession.id);
4866
5152
  const historyStartedAt = performance.now();
4867
- const productHistory = state.historyQuery.getProductHistoryCoverage(selectedSession.id);
5153
+ const productHistory = await (state.readQueries?.history ?? state.historyQuery).getProductHistoryCoverage(selectedSession.id);
4868
5154
  const historyInspection = requiresNativeHistoryCompatibility(selectedSession) && context.channelContext.inspectSessionRuntimeHistory
4869
5155
  ? await context.channelContext.inspectSessionRuntimeHistory(selectedSession.id).catch(() => undefined)
4870
5156
  : undefined;
4871
5157
  const historyMs = performance.now() - historyStartedAt;
4872
- const lastEventSequence = state.timelineQuery.getLatestEventSequence(selectedSession.id);
4873
- const latestStreamId = state.timelineQuery.getLatestStreamId({ piboSessionId: selectedSession.id });
5158
+ const lastEventSequence = await (state.readQueries?.timeline ?? state.timelineQuery).getLatestEventSequence(selectedSession.id);
5159
+ const latestStreamId = await (state.readQueries?.timeline ?? state.timelineQuery).getLatestStreamId({ piboSessionId: selectedSession.id });
4874
5160
  const version = createFastTraceV2Version({
4875
5161
  session: selectedSession,
4876
5162
  sessions: listSharedSessions(context),
@@ -4914,14 +5200,14 @@ export function createChatWebApp(options = {}) {
4914
5200
  const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined);
4915
5201
  if (timelineCursor.kind === "history")
4916
5202
  validateRuntimeHistoryCursor(selectedSession, timelineCursor);
4917
- state.sessionQuery.upsertSession(selectedSession);
5203
+ state.sessionQuery.upsertSessionsIfChanged([selectedSession]);
4918
5204
  const ownedSessions = listSharedSessions(context);
4919
5205
  const indexedSession = state.sessionQuery.getSession(selectedSession.id);
4920
5206
  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);
5207
+ const productHistory = await (state.readQueries?.history ?? state.historyQuery).getProductHistoryCoverage(selectedSession.id);
5208
+ const lastEventSequence = await (state.readQueries?.timeline ?? state.timelineQuery).getLatestEventSequence(selectedSession.id);
5209
+ const latestStreamId = await (state.readQueries?.timeline ?? state.timelineQuery).getLatestStreamId({ piboSessionId: selectedSession.id });
5210
+ const turnTimingScan = await (state.readQueries?.timeline ?? state.timelineQuery).scanMessageTurnTimings(selectedSession.id);
4925
5211
  const turnTimings = turnTimingScan.timings;
4926
5212
  const liveSnapshots = timelineCursor.kind === "tail" ? state.outputCompactor.snapshotsForSession(selectedSession.id) : [];
4927
5213
  const runtimeStatus = context.channelContext.getSessionRuntimeStatus
@@ -5023,7 +5309,7 @@ export function createChatWebApp(options = {}) {
5023
5309
  };
5024
5310
  }
5025
5311
  else {
5026
- const events = state.timelineQuery.listTraceEvents({
5312
+ const events = await (state.readQueries?.timeline ?? state.timelineQuery).listTraceEvents({
5027
5313
  piboSessionId: selectedSession.id,
5028
5314
  limit,
5029
5315
  ...(beforeSequence !== undefined ? { beforeSequence } : {}),
@@ -5041,7 +5327,7 @@ export function createChatWebApp(options = {}) {
5041
5327
  }
5042
5328
  const historyEntries = nativeHistory?.entries.length
5043
5329
  ? nativeHistory.entries
5044
- : state.historyQuery.listProductHistoryEntries({
5330
+ : await (state.readQueries?.history ?? state.historyQuery).listProductHistoryEntries({
5045
5331
  piboSessionId: selectedSession.id,
5046
5332
  limit: Math.min(limit * 2, 500),
5047
5333
  ...(beforeSequence !== undefined ? { beforeSequence } : {}),
@@ -5124,9 +5410,19 @@ export function createChatWebApp(options = {}) {
5124
5410
  if (!parsed)
5125
5411
  throw new PiboWebHttpError("Invalid trace payload ref", 400);
5126
5412
  resolveRequestedSession(state, context, webSession, defaultProfile, parsed.piboSessionId);
5413
+ if (url.searchParams.get("download") === "1") {
5414
+ const payload = state.dataStore.payloads.getPayload(parsed.payloadId);
5415
+ if (!payload)
5416
+ throw new PiboWebHttpError("Trace payload not found", 404);
5417
+ const body = Readable.toWeb(state.dataStore.payloads.openPayloadStream(parsed.payloadId));
5418
+ return new Response(body, { headers: {
5419
+ "content-type": "application/octet-stream", "content-disposition": 'attachment; filename="message-content.txt"',
5420
+ "content-length": String(payload.byteSize), "cache-control": "no-store", "x-content-type-options": "nosniff",
5421
+ } });
5422
+ }
5127
5423
  const offset = parseNonNegativeIntSearchParam(url, "offset", 0, Number.MAX_SAFE_INTEGER);
5128
5424
  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 });
5425
+ const chunk = await readTracePayloadChunk({ payloadStore: state.dataStore.payloads, ref, offset, limit });
5130
5426
  if (!chunk)
5131
5427
  throw new PiboWebHttpError("Trace payload not found", 404);
5132
5428
  return responseJson(chunk, { headers: { "cache-control": "no-store" } });
@@ -5139,7 +5435,7 @@ export function createChatWebApp(options = {}) {
5139
5435
  const beforeSequence = rawCursor.kind === "event" ? rawCursor.beforeSequence : undefined;
5140
5436
  const limit = parsePositiveIntSearchParam(url, "limit", TRACE_V2_RAW_EVENTS_DEFAULT_LIMIT, TRACE_V2_RAW_EVENTS_MAX_LIMIT);
5141
5437
  const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined);
5142
- const events = state.timelineQuery.listTraceEvents({
5438
+ const events = await (state.readQueries?.timeline ?? state.timelineQuery).listTraceEvents({
5143
5439
  piboSessionId: selectedSession.id,
5144
5440
  limit,
5145
5441
  ...(beforeSequence !== undefined ? { beforeSequence } : {}),
@@ -5161,11 +5457,11 @@ export function createChatWebApp(options = {}) {
5161
5457
  ? parsePositiveIntSearchParam(url, "pageSize", DEFAULT_TRACE_EVENTS_PAGE_SIZE, TRACE_V1_COMPAT_MAX_EVENTS_PER_REQUEST)
5162
5458
  : parsePositiveIntSearchParam(url, "eventLimit", DEFAULT_TRACE_EVENTS_PAGE_SIZE, TRACE_V1_COMPAT_MAX_EVENTS_PER_REQUEST);
5163
5459
  const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined);
5164
- state.sessionQuery.upsertSession(selectedSession);
5460
+ state.sessionQuery.upsertSessionsIfChanged([selectedSession]);
5165
5461
  const ownedSessions = listSharedSessions(context);
5166
5462
  const indexedSession = state.sessionQuery.getSession(selectedSession.id);
5167
5463
  let historyMs = 0;
5168
- const productHistory = state.historyQuery.getProductHistoryCoverage(selectedSession.id);
5464
+ const productHistory = await (state.readQueries?.history ?? state.historyQuery).getProductHistoryCoverage(selectedSession.id);
5169
5465
  let nativeHistory;
5170
5466
  if (beforeSequence === undefined
5171
5467
  && requiresNativeHistoryCompatibility(selectedSession)
@@ -5175,9 +5471,9 @@ export function createChatWebApp(options = {}) {
5175
5471
  nativeHistory = await context.channelContext.readSessionRuntimeHistory(selectedSession.id, { limit: Math.min(eventLimit * 4, 500) }).catch(() => undefined);
5176
5472
  historyMs += performance.now() - historyStartedAt;
5177
5473
  }
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);
5474
+ const lastEventSequence = await (state.readQueries?.timeline ?? state.timelineQuery).getLatestEventSequence(selectedSession.id);
5475
+ const latestStreamId = await (state.readQueries?.timeline ?? state.timelineQuery).getLatestStreamId({ piboSessionId: selectedSession.id });
5476
+ const turnTimingScan = await (state.readQueries?.timeline ?? state.timelineQuery).scanMessageTurnTimings(selectedSession.id);
5181
5477
  const turnTimings = turnTimingScan.timings;
5182
5478
  const liveSnapshots = beforeSequence === undefined ? state.outputCompactor.snapshotsForSession(selectedSession.id) : [];
5183
5479
  const runtimeStatus = context.channelContext.getSessionRuntimeStatus
@@ -5214,7 +5510,7 @@ export function createChatWebApp(options = {}) {
5214
5510
  let trace = cached;
5215
5511
  let eventCount = 0;
5216
5512
  if (!trace) {
5217
- const events = state.timelineQuery.listTraceEvents({
5513
+ const events = await (state.readQueries?.timeline ?? state.timelineQuery).listTraceEvents({
5218
5514
  piboSessionId: selectedSession.id,
5219
5515
  limit: eventLimit,
5220
5516
  ...(beforeSequence !== undefined ? { beforeSequence } : {}),
@@ -5222,7 +5518,7 @@ export function createChatWebApp(options = {}) {
5222
5518
  eventCount = events.length;
5223
5519
  const historyEntries = nativeHistory?.entries.length
5224
5520
  ? nativeHistory.entries
5225
- : state.historyQuery.listProductHistoryEntries({
5521
+ : await (state.readQueries?.history ?? state.historyQuery).listProductHistoryEntries({
5226
5522
  piboSessionId: selectedSession.id,
5227
5523
  limit: Math.min(eventLimit * 2, 1000),
5228
5524
  ...(beforeSequence !== undefined ? { beforeSequence } : {}),
@@ -5261,7 +5557,7 @@ export function createChatWebApp(options = {}) {
5261
5557
  }, { status: 413, headers: { ...baseHeaders, "x-pibo-trace-v1-deprecated": "true", ...serverTiming(cached ? "hit" : "miss", eventCount) } });
5262
5558
  }
5263
5559
  if (includeRawEvents) {
5264
- const rawEvents = state.timelineQuery.listTraceEvents({
5560
+ const rawEvents = await (state.readQueries?.timeline ?? state.timelineQuery).listTraceEvents({
5265
5561
  piboSessionId: selectedSession.id,
5266
5562
  limit: rawEventsLimit,
5267
5563
  ...(beforeSequence !== undefined ? { beforeSequence } : {}),
@@ -5281,7 +5577,10 @@ export function createChatWebApp(options = {}) {
5281
5577
  }
5282
5578
  if (url.pathname === `${CHAT_WEB_API_PREFIX}/debug/resources` && request.method === "GET") {
5283
5579
  await requireSession(request, context);
5284
- return responseJson({ gateway: serializeGatewayResourceDiagnostics(state) }, { headers: { "cache-control": "no-store" } });
5580
+ return responseJson({
5581
+ gateway: serializeGatewayResourceDiagnostics(state),
5582
+ storage: state.asyncStorage?.status() ?? { ready: true, mode: "in-process" },
5583
+ }, { headers: { "cache-control": "no-store" } });
5285
5584
  }
5286
5585
  if (url.pathname === `${CHAT_WEB_API_PREFIX}/debug/trace-at-sequence` && request.method === "POST") {
5287
5586
  requireSameOriginJsonRequest(request);
@@ -5297,12 +5596,12 @@ export function createChatWebApp(options = {}) {
5297
5596
  throw new PiboWebHttpError("Session not found", 404);
5298
5597
  const ownedSessions = listSharedSessions(context);
5299
5598
  const indexedSession = state.sessionQuery.getSession(piboSessionId);
5300
- const turnTimingScan = state.timelineQuery.scanMessageTurnTimings(piboSessionId);
5599
+ const turnTimingScan = await (state.readQueries?.timeline ?? state.timelineQuery).scanMessageTurnTimings(piboSessionId);
5301
5600
  const trace = await buildTraceView({
5302
5601
  session,
5303
5602
  sessions: ownedSessions,
5304
- events: state.timelineQuery.listTraceEvents({ piboSessionId, beforeOrAtSequence: eventSequence, limit: DEFAULT_TRACE_EVENTS_PAGE_SIZE }),
5305
- historyEntries: state.historyQuery.listProductHistoryEntries({
5603
+ events: await (state.readQueries?.timeline ?? state.timelineQuery).listTraceEvents({ piboSessionId, beforeOrAtSequence: eventSequence, limit: DEFAULT_TRACE_EVENTS_PAGE_SIZE }),
5604
+ historyEntries: await (state.readQueries?.history ?? state.historyQuery).listProductHistoryEntries({
5306
5605
  piboSessionId,
5307
5606
  limit: DEFAULT_TRACE_EVENTS_PAGE_SIZE,
5308
5607
  beforeSequence: eventSequence + 1,
@@ -5319,6 +5618,23 @@ export function createChatWebApp(options = {}) {
5319
5618
  const body = await readJsonBody(request);
5320
5619
  return startChatStreamingFixture({ state, context, webSession, defaultProfile, body });
5321
5620
  }
5621
+ if (url.pathname === `${CHAT_WEB_API_PREFIX}/message-receipts` && request.method === "GET") {
5622
+ const webSession = await requireSession(request, context);
5623
+ const sessionId = url.searchParams.get("piboSessionId") ?? "";
5624
+ if (!sessionId)
5625
+ throw new PiboWebHttpError("Session ID required", 400);
5626
+ resolveRequestedSession(state, context, webSession, defaultProfile, sessionId);
5627
+ return responseJson(await state.asyncStorage?.commandReceiptPage(sessionId) ?? { receipts: [] }, { headers: { "cache-control": "no-store" } });
5628
+ }
5629
+ if (url.pathname.startsWith(`${CHAT_WEB_API_PREFIX}/message-receipts/`) && request.method === "GET") {
5630
+ const webSession = await requireSession(request, context);
5631
+ const id = decodeURIComponent(url.pathname.slice(`${CHAT_WEB_API_PREFIX}/message-receipts/`.length));
5632
+ const receipt = await state.asyncStorage?.commandReceipt(id);
5633
+ if (!receipt)
5634
+ throw new PiboWebHttpError("Message receipt not found", 404);
5635
+ resolveRequestedSession(state, context, webSession, defaultProfile, receipt.sessionId, receipt.roomId);
5636
+ return responseJson({ receipt }, { headers: { "cache-control": "no-store" } });
5637
+ }
5322
5638
  if (url.pathname === `${CHAT_WEB_API_PREFIX}/message` && request.method === "POST") {
5323
5639
  requireSameOriginJsonRequest(request);
5324
5640
  const webSession = await requireSession(request, context);
@@ -5359,7 +5675,7 @@ export function createChatWebApp(options = {}) {
5359
5675
  if (!context.channelContext.getSessionStatusSnapshot) {
5360
5676
  throw new PiboWebHttpError("Session status snapshots are not available", 501);
5361
5677
  }
5362
- state.sessionQuery.upsertSession(selectedSession);
5678
+ state.sessionQuery.upsertSessionsIfChanged([selectedSession]);
5363
5679
  const snapshot = await context.channelContext.getSessionStatusSnapshot(selectedSession.id, url.searchParams.get("activate") === "false" ? { activate: false } : undefined);
5364
5680
  return responseJson(snapshot ?? { piboSessionId: selectedSession.id, runtimeActive: false }, {
5365
5681
  headers: { "cache-control": "no-store" },
@@ -5407,12 +5723,14 @@ export function createChatWebApp(options = {}) {
5407
5723
  if (isPiboRoomArchived(room)) {
5408
5724
  throw new PiboWebHttpError("Archived rooms are read-only", 403);
5409
5725
  }
5410
- state.sessionQuery.upsertSession(selectedSession);
5726
+ state.sessionQuery.upsertSessionsIfChanged([selectedSession]);
5727
+ const cancelledPending = body.action === "clear_queue" ? await state.asyncStorage?.cancelPendingCommands(selectedSession.id) ?? 0 : 0;
5411
5728
  const output = await context.channelContext.emit({
5412
5729
  type: "execution",
5413
5730
  piboSessionId: selectedSession.id,
5414
5731
  id: randomUUID(),
5415
5732
  action: body.action,
5733
+ ...(cancelledPending ? { clearedBeforeRuntime: cancelledPending } : {}),
5416
5734
  ...(body.params === undefined ? {} : { params: body.params }),
5417
5735
  });
5418
5736
  return responseJson(output);
@@ -5442,4 +5760,34 @@ export function createChatWebApp(options = {}) {
5442
5760
  return undefined;
5443
5761
  },
5444
5762
  };
5763
+ const uncachedHandle = application.handleRequest.bind(application);
5764
+ application.handleRequest = async (request, context) => {
5765
+ const url = new URL(request.url);
5766
+ const sid = url.searchParams.get("piboSessionId");
5767
+ const structure = context.channelContext.getSessionStructureRevision?.();
5768
+ 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))
5769
+ return uncachedHandle(request, context);
5770
+ const webSession = await requireSession(request, context);
5771
+ const selected = resolveRequestedSession(state, context, webSession, defaultProfile, sid);
5772
+ if (requiresNativeHistoryCompatibility(selected))
5773
+ return uncachedHandle(request, context);
5774
+ url.searchParams.sort();
5775
+ const fingerprint = () => {
5776
+ const session = context.channelContext.getSession(sid);
5777
+ const revision = state.dataStore.db.prepare("SELECT revision FROM chat_trace_revisions WHERE session_id=?").get(sid);
5778
+ const history = state.dataStore.db.prepare("SELECT revision FROM chat_history_counts WHERE session_id=?").get(sid);
5779
+ const backfill = state.dataStore.db.prepare("SELECT cursor,target FROM chat_read_backfill WHERE id=1").get();
5780
+ const runtime = context.channelContext.getSessionRuntimeStatus?.(sid);
5781
+ 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 });
5782
+ };
5783
+ const key = fingerprint();
5784
+ const cached = earlyTraceCache.get(key, request);
5785
+ if (cached)
5786
+ return cached;
5787
+ const response = await uncachedHandle(request, context);
5788
+ if (response && fingerprint() === key)
5789
+ await earlyTraceCache.set(key, response);
5790
+ return response;
5791
+ };
5792
+ return application;
5445
5793
  }