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