@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.
- package/dist/agent-runtime/routed-session.js +111 -25
- package/dist/agent-runtimes/codex-native/turn.js +27 -3
- package/dist/agent-runtimes/pi/adapter.js +1 -0
- package/dist/agent-runtimes/pi/routed-session.js +2 -2
- package/dist/apps/chat/bounded-event-stream.js +98 -0
- package/dist/apps/chat/chat-settings-routes.js +3 -3
- package/dist/apps/chat/data/chat-data-mappers.js +40 -14
- package/dist/apps/chat/data/event-command-service.js +30 -21
- package/dist/apps/chat/data/history-query-service.js +34 -25
- package/dist/apps/chat/data/read-state-service.js +37 -3
- package/dist/apps/chat/data/session-query-service.js +19 -11
- package/dist/apps/chat/data/timeline-query-service.js +19 -7
- package/dist/apps/chat/message-command-dispatcher.js +149 -0
- package/dist/apps/chat/output-compactor.js +9 -0
- package/dist/apps/chat/output-event-policy.js +9 -1
- package/dist/apps/chat/stream.js +21 -3
- package/dist/apps/chat/telemetry-retention-service.js +113 -8
- package/dist/apps/chat/trace-response-cache.js +46 -0
- package/dist/apps/chat/trace-v2.js +12 -6
- package/dist/apps/chat/trace.js +1 -1
- package/dist/apps/chat/web-app.js +645 -297
- package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-B-auLrzD.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-BA_dsINH.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-eJZar_0-.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-wNNR2Bci.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-zcmEsIEp.js} +1 -1
- package/dist/apps/chat-ui/assets/index-DEkbN5Vo.js +229 -0
- package/dist/apps/chat-ui/assets/index-DZK6Tzil.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-DZgW1fCB.js +44 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/cli-session/localSessionSource.js +3 -2
- package/dist/compute/pool/seeds.js +25 -3
- package/dist/core/events.js +4 -0
- package/dist/core/output-render-sequence.js +65 -6
- package/dist/core/provider-capacity.js +33 -0
- package/dist/core/provider-telemetry.js +21 -6
- package/dist/core/runtime-capacity.js +174 -0
- package/dist/core/runtime-telemetry.js +40 -12
- package/dist/core/session-router.js +178 -13
- package/dist/data/async-chat-reads.js +38 -0
- package/dist/data/async-chat-storage.js +96 -0
- package/dist/data/async-telemetry-maintenance.js +9 -0
- package/dist/data/bounded-worker-client.js +256 -0
- package/dist/data/chat-read-projections.js +159 -0
- package/dist/data/chat-read-worker.js +73 -0
- package/dist/data/chat-storage-worker.js +167 -0
- package/dist/data/ingest-service.js +152 -18
- package/dist/data/message-command-store.js +290 -0
- package/dist/data/payload-store.js +92 -11
- package/dist/data/pibo-store.js +10 -8
- package/dist/data/schema.js +39 -4
- package/dist/data/session-store.js +3 -1
- package/dist/data/storage-backup.js +278 -0
- package/dist/data/storage-maintenance.js +344 -0
- package/dist/data/storage-verification-worker.js +25 -0
- package/dist/data/telemetry-capture.js +188 -0
- package/dist/data/telemetry-command.js +3 -0
- package/dist/data/telemetry-maintenance-worker.js +40 -0
- package/dist/data/telemetry-maintenance.js +110 -0
- package/dist/data/telemetry-retention.js +16 -7
- package/dist/data/telemetry-worker.js +111 -0
- package/dist/data/telemetry-writer.js +150 -83
- package/dist/data/telemetry.js +5 -0
- package/dist/debug/index.js +207 -1
- package/dist/debug/message-queue.js +108 -0
- package/dist/debug/output-collision-repair.js +140 -0
- package/dist/debug/output-integrity.js +38 -2
- package/dist/debug/output-repair.js +1 -0
- package/dist/debug/storage-backup.js +41 -0
- package/dist/debug/storage-maintenance.js +78 -0
- package/dist/debug/telemetry-capture.js +66 -0
- package/dist/gateway/cli.js +71 -7
- package/dist/gateway/server.js +3 -0
- package/dist/providers/openai-gpt56.js +11 -6
- package/dist/reliability/store.js +119 -23
- package/dist/session-ui/terminalRows.js +54 -13
- package/dist/sessions/pibo-data-store.js +19 -14
- package/dist/shared/debug-features.js +4 -0
- package/dist/shared/model-inference-metrics.js +23 -0
- package/dist/shared/trace-event-projection.js +69 -2
- package/dist/shared/trace-history.js +9 -0
- package/dist/shared/trace-live-reducer.js +1 -0
- package/dist/shared/trace-patch-nodes.js +19 -0
- package/dist/web/channel.js +117 -12
- package/dist/web/http.js +135 -41
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-BOceJ0jM.css +0 -1
- package/dist/apps/chat-ui/assets/index-BOemYq-V.js +0 -228
- 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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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
|
-
|
|
518
|
-
|
|
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
|
-
|
|
579
|
+
// The durable receipt is authoritative; the job can now finish without another payload rewrite.
|
|
549
580
|
}
|
|
550
581
|
else if (!delivery.sideEffectsDelivered) {
|
|
551
|
-
|
|
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
|
-
|
|
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
|
|
813
|
+
const revision = context.channelContext.getSessionStructureRevision?.();
|
|
759
814
|
const profiles = context.channelContext.getProfiles?.();
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
|
2811
|
-
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
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,
|
|
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
|
|
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, {
|
|
2868
|
-
: signalStatusFromSnapshot(snapshotSignalSession?.(session.id), session.id, {
|
|
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
|
|
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 =
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
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
|
-
|
|
3182
|
-
|
|
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
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
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
|
|
3211
|
-
if (!
|
|
3340
|
+
for (const stored of events) {
|
|
3341
|
+
if (!await stream.waitForCapacity())
|
|
3212
3342
|
return;
|
|
3213
|
-
writeChatEventFrames(controller,
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
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
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
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
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
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
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
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
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
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
|
-
|
|
3501
|
-
|
|
3502
|
-
delivery,
|
|
3503
|
-
source: "user",
|
|
3630
|
+
messageText: text,
|
|
3631
|
+
attachmentIds: input.body.webAnnotationIds,
|
|
3504
3632
|
});
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
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.
|
|
3515
|
-
actorType: "
|
|
3641
|
+
eventType: "user.message.accepted",
|
|
3642
|
+
actorType: "user",
|
|
3516
3643
|
actorId,
|
|
3517
|
-
|
|
3644
|
+
clientTxnId,
|
|
3645
|
+
retentionClass: "chat_message",
|
|
3518
3646
|
payload: {
|
|
3519
|
-
type: "user.message.
|
|
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(
|
|
3528
|
-
if (
|
|
3529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
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
|
-
|
|
3813
|
-
|
|
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
|
|
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
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
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
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
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
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
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
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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({
|
|
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.
|
|
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.
|
|
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
|
}
|