@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,4 @@
|
|
|
1
|
+
import { previouslyClearedMessages } from "../core/events.js";
|
|
1
2
|
import { PiboSteeringUnavailableError, } from "../core/events.js";
|
|
2
3
|
import { PIBO_PROVIDER_RECOVERY_PROMPT, isPiboProviderFallbackError } from "../core/provider-recovery.js";
|
|
3
4
|
import { normalizeSessionErrorDetails, runtimeSessionErrorDetails } from "../core/session-errors.js";
|
|
@@ -17,6 +18,35 @@ const RUN_REMINDER_MAX_PROVIDER_ROUNDS = 64;
|
|
|
17
18
|
const RUN_REMINDER_MAX_ACTIVE_TOKENS = 2_000_000;
|
|
18
19
|
export const RUN_REMINDER_MAX_DURATION_MS = 15 * 60 * 1000;
|
|
19
20
|
const RUN_REMINDER_MAX_REPEATED_TOOL_CALLS = 12;
|
|
21
|
+
const RUNTIME_QUEUE_MAX_MESSAGE_BYTES = 1024 * 1024;
|
|
22
|
+
const RUNTIME_QUEUE_MAX_MESSAGES = 64;
|
|
23
|
+
const RUNTIME_QUEUE_MAX_BYTES = 4 * 1024 * 1024;
|
|
24
|
+
const RUNTIME_QUEUE_MAX_WAIT_MS = 10 * 60 * 1000;
|
|
25
|
+
export class RuntimeQueueCapacityError extends Error {
|
|
26
|
+
dimension;
|
|
27
|
+
current;
|
|
28
|
+
limit;
|
|
29
|
+
code = "runtime_capacity_unavailable";
|
|
30
|
+
constructor(dimension, current, limit) {
|
|
31
|
+
super(`Session runtime queue ${dimension} capacity reached (current=${capacityDimensionCurrent(dimension, current)}, limit=${limit}).`);
|
|
32
|
+
this.dimension = dimension;
|
|
33
|
+
this.current = current;
|
|
34
|
+
this.limit = limit;
|
|
35
|
+
this.name = "RuntimeQueueCapacityError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function capacityDimensionCurrent(dimension, current) {
|
|
39
|
+
if (dimension === "message_bytes")
|
|
40
|
+
return current.messageBytes;
|
|
41
|
+
if (dimension === "queue_count")
|
|
42
|
+
return current.queueCount;
|
|
43
|
+
if (dimension === "queue_bytes")
|
|
44
|
+
return current.queueBytes;
|
|
45
|
+
return current.oldestWaitMs;
|
|
46
|
+
}
|
|
47
|
+
function isRunReminderMessage(event) {
|
|
48
|
+
return event.source === "service" && event.text.startsWith("<pibo_run_notification>");
|
|
49
|
+
}
|
|
20
50
|
function serializedToolArgs(value) {
|
|
21
51
|
try {
|
|
22
52
|
return JSON.stringify(value) ?? String(value);
|
|
@@ -146,6 +176,7 @@ export class RuntimeRoutedSession {
|
|
|
146
176
|
modelFallbacks;
|
|
147
177
|
suppressProviderFailures = false;
|
|
148
178
|
pendingProviderFailure;
|
|
179
|
+
runReminderDeferredWarning;
|
|
149
180
|
unsubscribe;
|
|
150
181
|
constructor(piboSessionId, runtimeSession, emit, pluginRegistry, options = {}) {
|
|
151
182
|
this.piboSessionId = piboSessionId;
|
|
@@ -166,8 +197,30 @@ export class RuntimeRoutedSession {
|
|
|
166
197
|
if (this.sessionIdentityOperationInFlight && !this.forkCandidatesRequest) {
|
|
167
198
|
throw new Error("Pibo session cannot accept messages while a session identity operation is in progress.");
|
|
168
199
|
}
|
|
200
|
+
if (isRunReminderMessage(event))
|
|
201
|
+
this.removeQueuedMessages(isRunReminderMessage);
|
|
202
|
+
const now = this.options.now?.() ?? Date.now();
|
|
203
|
+
// Run reminders are coalesced service work. They remain bounded on admission,
|
|
204
|
+
// but cannot age-poison the ordinary message budget while an active turn runs.
|
|
205
|
+
const messages = this.queue.filter((item) => item.kind === "message" && !isRunReminderMessage(item.event));
|
|
206
|
+
const messageBytes = Buffer.byteLength(event.text);
|
|
207
|
+
const queueBytes = messages.reduce((total, item) => total + Buffer.byteLength(item.event.text), messageBytes);
|
|
208
|
+
const oldestWaitMs = messages.reduce((oldest, item) => Math.max(oldest, now - item.acceptedAt), 0);
|
|
209
|
+
const current = { messageBytes, queueCount: messages.length, queueBytes, oldestWaitMs };
|
|
210
|
+
if (messageBytes > RUNTIME_QUEUE_MAX_MESSAGE_BYTES) {
|
|
211
|
+
throw new RuntimeQueueCapacityError("message_bytes", current, RUNTIME_QUEUE_MAX_MESSAGE_BYTES);
|
|
212
|
+
}
|
|
213
|
+
if (messages.length >= RUNTIME_QUEUE_MAX_MESSAGES) {
|
|
214
|
+
throw new RuntimeQueueCapacityError("queue_count", current, RUNTIME_QUEUE_MAX_MESSAGES);
|
|
215
|
+
}
|
|
216
|
+
if (queueBytes > RUNTIME_QUEUE_MAX_BYTES) {
|
|
217
|
+
throw new RuntimeQueueCapacityError("queue_bytes", current, RUNTIME_QUEUE_MAX_BYTES);
|
|
218
|
+
}
|
|
219
|
+
if (oldestWaitMs >= RUNTIME_QUEUE_MAX_WAIT_MS) {
|
|
220
|
+
throw new RuntimeQueueCapacityError("oldest_wait_age", current, RUNTIME_QUEUE_MAX_WAIT_MS);
|
|
221
|
+
}
|
|
169
222
|
onAccepted();
|
|
170
|
-
this.queue.push({ kind: "message", event });
|
|
223
|
+
this.queue.push({ kind: "message", event, acceptedAt: now });
|
|
171
224
|
const output = {
|
|
172
225
|
type: "message_queued",
|
|
173
226
|
piboSessionId: this.piboSessionId,
|
|
@@ -310,7 +363,9 @@ export class RuntimeRoutedSession {
|
|
|
310
363
|
thinkingLevel,
|
|
311
364
|
fastMode: status.fastMode?.mode === "fast",
|
|
312
365
|
retry: status.retry,
|
|
313
|
-
warnings:
|
|
366
|
+
warnings: this.runReminderDeferredWarning
|
|
367
|
+
? [...new Set([...(status.warnings ?? []), this.runReminderDeferredWarning])]
|
|
368
|
+
: status.warnings,
|
|
314
369
|
errors: status.errors,
|
|
315
370
|
...(pendingApprovals.length > 0
|
|
316
371
|
? { pendingApprovals: pendingApprovals.map((request) => structuredClone(request)) }
|
|
@@ -329,7 +384,9 @@ export class RuntimeRoutedSession {
|
|
|
329
384
|
activeModel: status.activeModel,
|
|
330
385
|
contextUsage: status.contextUsage,
|
|
331
386
|
providerUsage: status.providerUsage,
|
|
332
|
-
warnings:
|
|
387
|
+
warnings: this.runReminderDeferredWarning
|
|
388
|
+
? [...new Set([...(status.warnings ?? []), this.runReminderDeferredWarning])]
|
|
389
|
+
: status.warnings,
|
|
333
390
|
errors: status.errors,
|
|
334
391
|
};
|
|
335
392
|
}
|
|
@@ -376,8 +433,13 @@ export class RuntimeRoutedSession {
|
|
|
376
433
|
}
|
|
377
434
|
removedMessages.reverse();
|
|
378
435
|
this.notifyMessagesInterrupted(removedMessages, "queued message removed");
|
|
436
|
+
if (removedMessages.length > 0)
|
|
437
|
+
this.notifyState();
|
|
379
438
|
return removedMessages.length;
|
|
380
439
|
}
|
|
440
|
+
setRunReminderDeferredWarning(message) {
|
|
441
|
+
this.runReminderDeferredWarning = message;
|
|
442
|
+
}
|
|
381
443
|
getCurrentSession() {
|
|
382
444
|
const getCurrentSession = this.runtimeSession.controls?.getCurrentSession;
|
|
383
445
|
if (!getCurrentSession)
|
|
@@ -593,6 +655,7 @@ export class RuntimeRoutedSession {
|
|
|
593
655
|
if (inFlight?.event.id === eventId) {
|
|
594
656
|
if (!inFlight.cancellation) {
|
|
595
657
|
inFlight.cancelled = true;
|
|
658
|
+
inFlight.capacityAbort.abort(new Error("Message cancelled while waiting for capacity."));
|
|
596
659
|
this.notifyMessagesInterrupted([inFlight.event], "message cancelled");
|
|
597
660
|
const active = this.activeMessage?.id === eventId;
|
|
598
661
|
inFlight.cancellation = (async () => {
|
|
@@ -618,24 +681,23 @@ export class RuntimeRoutedSession {
|
|
|
618
681
|
}
|
|
619
682
|
return false;
|
|
620
683
|
}
|
|
684
|
+
runtimeAssistantIndices = new Map();
|
|
621
685
|
handleRuntimeEvent(event) {
|
|
622
686
|
switch (event.type) {
|
|
623
687
|
case "assistant_delta":
|
|
624
|
-
|
|
625
|
-
|
|
688
|
+
case "assistant_message": {
|
|
689
|
+
const output = this.withActiveMessage({
|
|
690
|
+
type: event.type,
|
|
626
691
|
piboSessionId: this.piboSessionId,
|
|
627
692
|
text: event.text,
|
|
628
693
|
contentIndex: event.contentIndex,
|
|
629
|
-
})
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
piboSessionId: this.piboSessionId,
|
|
635
|
-
text: event.text,
|
|
636
|
-
contentIndex: event.contentIndex,
|
|
637
|
-
}));
|
|
694
|
+
});
|
|
695
|
+
if ((output.type === "assistant_delta" || output.type === "assistant_message") && event.contentIndex !== undefined && output.assistantIndex !== undefined) {
|
|
696
|
+
this.runtimeAssistantIndices.set(event.contentIndex, output.assistantIndex);
|
|
697
|
+
}
|
|
698
|
+
this.emit(output);
|
|
638
699
|
return;
|
|
700
|
+
}
|
|
639
701
|
case "reasoning_started":
|
|
640
702
|
this.emit(this.withActiveMessage({
|
|
641
703
|
type: "thinking_started",
|
|
@@ -677,6 +739,12 @@ export class RuntimeRoutedSession {
|
|
|
677
739
|
this.emit(this.withActiveMessage({
|
|
678
740
|
type: "assistant_usage",
|
|
679
741
|
piboSessionId: this.piboSessionId,
|
|
742
|
+
...(event.inferenceId ? { inferenceId: event.inferenceId } : {}),
|
|
743
|
+
...(event.target ? { inferenceTarget: event.target.type === "assistant"
|
|
744
|
+
? (this.runtimeAssistantIndices.has(event.target.contentIndex)
|
|
745
|
+
? { type: "assistant", assistantIndex: this.runtimeAssistantIndices.get(event.target.contentIndex) }
|
|
746
|
+
: { type: "turn" })
|
|
747
|
+
: event.target } : {}),
|
|
680
748
|
inputTokens: event.usage.inputTokens,
|
|
681
749
|
outputTokens: event.usage.outputTokens,
|
|
682
750
|
cacheReadTokens: event.usage.cacheReadTokens,
|
|
@@ -710,6 +778,7 @@ export class RuntimeRoutedSession {
|
|
|
710
778
|
result: event.result,
|
|
711
779
|
aborted: event.aborted,
|
|
712
780
|
errorMessage: event.errorMessage,
|
|
781
|
+
compactionStats: event.compactionStats,
|
|
713
782
|
}));
|
|
714
783
|
return;
|
|
715
784
|
case "approval_requested":
|
|
@@ -815,11 +884,29 @@ export class RuntimeRoutedSession {
|
|
|
815
884
|
this.notifyState();
|
|
816
885
|
}
|
|
817
886
|
}
|
|
887
|
+
async promptWithinCapacity(event, inFlight, input) {
|
|
888
|
+
const provider = this.runtimeSession.getStatus().activeModel?.provider ?? this.runtimeSession.runtimeInstanceId;
|
|
889
|
+
const lease = await this.options.acquireProviderCapacity?.(provider, inFlight.capacityAbort.signal);
|
|
890
|
+
try {
|
|
891
|
+
if (this.disposed || inFlight.cancelled)
|
|
892
|
+
return;
|
|
893
|
+
if (!inFlight.started) {
|
|
894
|
+
inFlight.started = true;
|
|
895
|
+
this.emit({ type: "message_started", piboSessionId: this.piboSessionId, eventId: event.id, text: event.text, source: event.source, provenance: event.provenance });
|
|
896
|
+
}
|
|
897
|
+
if (this.disposed || inFlight.cancelled)
|
|
898
|
+
return;
|
|
899
|
+
await this.runtimeSession.prompt(input);
|
|
900
|
+
}
|
|
901
|
+
finally {
|
|
902
|
+
lease?.release();
|
|
903
|
+
}
|
|
904
|
+
}
|
|
818
905
|
async promptWithModelFallbacks(event, inFlight) {
|
|
819
906
|
const canSetModel = Boolean(this.runtimeSession.controls?.setModel);
|
|
820
907
|
const fallbackModels = this.modelFallbacks.filter((model) => !sameModel(model, this.primaryModel));
|
|
821
908
|
if (!canSetModel || fallbackModels.length === 0) {
|
|
822
|
-
await this.
|
|
909
|
+
await this.promptWithinCapacity(event, inFlight, {
|
|
823
910
|
text: event.text,
|
|
824
911
|
source: promptSource(event.source),
|
|
825
912
|
capabilityScope: event.capabilityScope,
|
|
@@ -845,7 +932,7 @@ export class RuntimeRoutedSession {
|
|
|
845
932
|
this.pendingProviderFailure = undefined;
|
|
846
933
|
let promptError;
|
|
847
934
|
try {
|
|
848
|
-
await this.
|
|
935
|
+
await this.promptWithinCapacity(event, inFlight, {
|
|
849
936
|
text: attempt === 0 ? event.text : PIBO_PROVIDER_RECOVERY_PROMPT,
|
|
850
937
|
source: attempt === 0 ? promptSource(event.source) : "rpc",
|
|
851
938
|
capabilityScope: event.capabilityScope,
|
|
@@ -907,14 +994,6 @@ export class RuntimeRoutedSession {
|
|
|
907
994
|
this.activeMessageFailed = false;
|
|
908
995
|
this.beginRunReminderTurnGuard(event);
|
|
909
996
|
this.resetContentIndices();
|
|
910
|
-
this.emit({
|
|
911
|
-
type: "message_started",
|
|
912
|
-
piboSessionId: this.piboSessionId,
|
|
913
|
-
eventId: event.id,
|
|
914
|
-
text: event.text,
|
|
915
|
-
source: event.source,
|
|
916
|
-
provenance: event.provenance,
|
|
917
|
-
});
|
|
918
997
|
if (inFlight.cancelled)
|
|
919
998
|
return;
|
|
920
999
|
await this.promptWithModelFallbacks(event, inFlight);
|
|
@@ -963,6 +1042,7 @@ export class RuntimeRoutedSession {
|
|
|
963
1042
|
const inFlight = {
|
|
964
1043
|
event,
|
|
965
1044
|
cancelled: false,
|
|
1045
|
+
capacityAbort: new AbortController(),
|
|
966
1046
|
settled: new Promise((resolve) => { resolveSettled = resolve; }),
|
|
967
1047
|
resolveSettled: () => { resolveSettled?.(); },
|
|
968
1048
|
};
|
|
@@ -1129,8 +1209,12 @@ export class RuntimeRoutedSession {
|
|
|
1129
1209
|
? await this.options.logoutRuntimeAuth(input)
|
|
1130
1210
|
: await this.pluginRegistry.logoutAgentRuntimeAuth(this.runtimeSession.runtimeInstanceId, input),
|
|
1131
1211
|
getProviderUsage: () => this.getActionProviderUsage(),
|
|
1132
|
-
clearQueue: () => this.clearQueue(),
|
|
1212
|
+
clearQueue: () => this.clearQueue() + previouslyClearedMessages(event),
|
|
1133
1213
|
abort: async () => {
|
|
1214
|
+
if (this.inFlightMessage && !this.inFlightMessage.started) {
|
|
1215
|
+
await this.cancelMessage(this.inFlightMessage.event.id);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1134
1218
|
if (this.activeMessage)
|
|
1135
1219
|
this.notifyMessagesInterrupted([this.activeMessage], "abort requested");
|
|
1136
1220
|
await this.runtimeSession.abort();
|
|
@@ -1187,6 +1271,7 @@ export class RuntimeRoutedSession {
|
|
|
1187
1271
|
if (this.disposed)
|
|
1188
1272
|
return false;
|
|
1189
1273
|
const activeMessage = this.activeMessage;
|
|
1274
|
+
this.inFlightMessage?.capacityAbort.abort(new Error(reason));
|
|
1190
1275
|
this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), reason);
|
|
1191
1276
|
if (activeMessage) {
|
|
1192
1277
|
const error = `Session disposed while a message was active: ${reason}`;
|
|
@@ -1262,6 +1347,7 @@ export class RuntimeRoutedSession {
|
|
|
1262
1347
|
}
|
|
1263
1348
|
resetContentIndices() {
|
|
1264
1349
|
this.closeActiveContentParts();
|
|
1350
|
+
this.runtimeAssistantIndices.clear();
|
|
1265
1351
|
this.nextAssistantIndex = 0;
|
|
1266
1352
|
this.nextThinkingIndex = 0;
|
|
1267
1353
|
}
|
|
@@ -245,6 +245,9 @@ function newPendingTurn(kind = "turn") {
|
|
|
245
245
|
compactionEnded: false,
|
|
246
246
|
startedTools: new Map(),
|
|
247
247
|
completedTools: new Set(),
|
|
248
|
+
usageSnapshots: new Set(),
|
|
249
|
+
usageItemOffset: 0,
|
|
250
|
+
nextUsageIndex: 0,
|
|
248
251
|
};
|
|
249
252
|
}
|
|
250
253
|
export class CodexNativeTurnController {
|
|
@@ -611,7 +614,30 @@ export class CodexNativeTurnController {
|
|
|
611
614
|
const params = this.scopedTurnParams(value, "thread token usage");
|
|
612
615
|
if (!params)
|
|
613
616
|
return;
|
|
614
|
-
|
|
617
|
+
const snapshot = requiredRecord(params.record.tokenUsage, "thread token usage");
|
|
618
|
+
const usage = validateTokenUsage(snapshot);
|
|
619
|
+
const total = tokenUsageBreakdown(snapshot.total, "cumulative thread usage");
|
|
620
|
+
const key = JSON.stringify(total);
|
|
621
|
+
const pending = params.pending;
|
|
622
|
+
// Codex repeats snapshots for quota/replay notifications. Equal last usage
|
|
623
|
+
// alone is not a duplicate: two real requests may use identical token counts.
|
|
624
|
+
if (pending.usageSnapshots.has(key))
|
|
625
|
+
return;
|
|
626
|
+
pending.usageSnapshots.add(key);
|
|
627
|
+
const newItems = pending.itemOrder.slice(pending.usageItemOffset);
|
|
628
|
+
pending.usageItemOffset = pending.itemOrder.length;
|
|
629
|
+
let target = { type: "turn" };
|
|
630
|
+
// A usage update closes the observed output span. Only newly introduced
|
|
631
|
+
// items belong to it; late tool completions cannot steal another step's usage.
|
|
632
|
+
// Several tools in one response share one record, anchored at its last output.
|
|
633
|
+
for (const id of newItems) {
|
|
634
|
+
const item = pending.items.get(id);
|
|
635
|
+
if (TOOL_ITEM_TYPES.has(item.type))
|
|
636
|
+
target = { type: "tool", toolCallId: id };
|
|
637
|
+
else if (item.type === "agentMessage")
|
|
638
|
+
target = { type: "assistant", contentIndex: this.assistantIndex(pending, id) };
|
|
639
|
+
}
|
|
640
|
+
this.emit({ type: "usage", usage, inferenceId: `${pending.turnId}:usage:${pending.nextUsageIndex++}`, target });
|
|
615
641
|
}
|
|
616
642
|
handleThreadStatus(value) {
|
|
617
643
|
const params = requiredRecord(value, "thread status notification");
|
|
@@ -845,8 +871,6 @@ export class CodexNativeTurnController {
|
|
|
845
871
|
for (const item of turn.items)
|
|
846
872
|
this.completeItem(pending, item);
|
|
847
873
|
this.finishIncompleteItems(pending, turn.status);
|
|
848
|
-
if (pending.usage)
|
|
849
|
-
this.emit({ type: "usage", usage: pending.usage });
|
|
850
874
|
const persistedItems = pending.itemOrder
|
|
851
875
|
.map((itemId) => pending.items.get(itemId))
|
|
852
876
|
.filter((item) => item !== undefined);
|
|
@@ -253,6 +253,7 @@ export function semanticEventFromPibo(event) {
|
|
|
253
253
|
result: event.result,
|
|
254
254
|
aborted: event.aborted,
|
|
255
255
|
errorMessage: event.errorMessage,
|
|
256
|
+
compactionStats: event.compactionStats,
|
|
256
257
|
};
|
|
257
258
|
case "session_error":
|
|
258
259
|
return { type: "error", message: event.error, details: event.errorDetails };
|
|
@@ -863,8 +863,6 @@ export class RoutedSession {
|
|
|
863
863
|
if (assistantMessageEnded && !assistantMessageFailed)
|
|
864
864
|
this.flushPendingProviderWebSearchFinishes();
|
|
865
865
|
const usageEvent = assistantMessageEnded ? normalizeAssistantUsageEvent(this.piboSessionId, candidate?.message) : undefined;
|
|
866
|
-
if (usageEvent)
|
|
867
|
-
this.emit(this.withActiveMessage(usageEvent));
|
|
868
866
|
// Pi gets the first chance to recover through its short retry/compaction loop.
|
|
869
867
|
// Keep the final error pending so the routed turn can continue durable recovery.
|
|
870
868
|
if (assistantMessageEnded && normalized?.type === "session_error") {
|
|
@@ -889,6 +887,8 @@ export class RoutedSession {
|
|
|
889
887
|
this.emit(this.withActiveMessage(normalized));
|
|
890
888
|
}
|
|
891
889
|
}
|
|
890
|
+
if (usageEvent)
|
|
891
|
+
this.emit(this.withActiveMessage(usageEvent));
|
|
892
892
|
if (this.forwardPiEvents) {
|
|
893
893
|
this.emit({ type: "pi_event", piboSessionId: this.piboSessionId, event });
|
|
894
894
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Owns the only application queue; HWM zero prevents a second hidden stream queue. */
|
|
2
|
+
export class BoundedEventStream {
|
|
3
|
+
onClose;
|
|
4
|
+
maximumBytes;
|
|
5
|
+
maximumAgeMs;
|
|
6
|
+
maximumFrames;
|
|
7
|
+
stream;
|
|
8
|
+
writer;
|
|
9
|
+
controller;
|
|
10
|
+
queue = [];
|
|
11
|
+
bytes = 0;
|
|
12
|
+
stopped = false;
|
|
13
|
+
finishing = false;
|
|
14
|
+
pendingPull;
|
|
15
|
+
capacityWaiter;
|
|
16
|
+
timer;
|
|
17
|
+
constructor(onClose, maximumBytes = 2 * 1024 * 1024, maximumAgeMs = 5000, maximumFrames = 4096) {
|
|
18
|
+
this.onClose = onClose;
|
|
19
|
+
this.maximumBytes = maximumBytes;
|
|
20
|
+
this.maximumAgeMs = maximumAgeMs;
|
|
21
|
+
this.maximumFrames = maximumFrames;
|
|
22
|
+
this.stream = new ReadableStream({
|
|
23
|
+
start: controller => { this.controller = controller; },
|
|
24
|
+
pull: () => {
|
|
25
|
+
const frame = this.queue.shift();
|
|
26
|
+
if (frame) {
|
|
27
|
+
this.bytes -= frame.value.byteLength;
|
|
28
|
+
this.controller.enqueue(frame.value);
|
|
29
|
+
this.releaseCapacity();
|
|
30
|
+
if (this.finishing && !this.queue.length)
|
|
31
|
+
this.stop("complete");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (this.finishing || this.stopped) {
|
|
35
|
+
this.stop("complete");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
return new Promise(resolve => { this.pendingPull = resolve; });
|
|
39
|
+
},
|
|
40
|
+
cancel: () => this.stop("cancel"),
|
|
41
|
+
}, { highWaterMark: 0 });
|
|
42
|
+
const self = this;
|
|
43
|
+
this.writer = { enqueue: value => { this.enqueue(value); }, close: () => this.finish(), error: () => this.stop("error"), get desiredSize() { return self.stopped ? null : self.maximumBytes - self.bytes; } };
|
|
44
|
+
this.timer = setInterval(() => { if (this.queue.length && performance.now() - this.queue[0].at > this.maximumAgeMs)
|
|
45
|
+
this.stop("slow_age"); }, Math.min(1000, Math.max(5, maximumAgeMs / 2)));
|
|
46
|
+
this.timer.unref();
|
|
47
|
+
}
|
|
48
|
+
get closed() { return this.stopped || this.finishing; }
|
|
49
|
+
enqueue(value) {
|
|
50
|
+
if (this.closed)
|
|
51
|
+
return;
|
|
52
|
+
if (value.byteLength > this.maximumBytes || this.bytes + value.byteLength > this.maximumBytes || this.queue.length >= this.maximumFrames) {
|
|
53
|
+
this.stop("slow_bytes");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (this.pendingPull) {
|
|
57
|
+
const resolve = this.pendingPull;
|
|
58
|
+
this.pendingPull = undefined;
|
|
59
|
+
this.controller.enqueue(value);
|
|
60
|
+
resolve();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.queue.push({ value, at: performance.now() });
|
|
64
|
+
this.bytes += value.byteLength;
|
|
65
|
+
}
|
|
66
|
+
async waitForCapacity() { if (this.closed)
|
|
67
|
+
return false; if (this.bytes < this.maximumBytes / 2)
|
|
68
|
+
return true; return new Promise(resolve => { this.capacityWaiter = resolve; }); }
|
|
69
|
+
releaseCapacity() { if (this.bytes < this.maximumBytes / 2) {
|
|
70
|
+
this.capacityWaiter?.(!this.closed);
|
|
71
|
+
this.capacityWaiter = undefined;
|
|
72
|
+
} }
|
|
73
|
+
finish() { if (this.stopped)
|
|
74
|
+
return; this.finishing = true; if (!this.queue.length)
|
|
75
|
+
this.stop("complete"); }
|
|
76
|
+
fail() { this.stop("error"); }
|
|
77
|
+
stop(reason) {
|
|
78
|
+
if (this.stopped)
|
|
79
|
+
return;
|
|
80
|
+
this.stopped = true;
|
|
81
|
+
clearInterval(this.timer);
|
|
82
|
+
this.queue.length = 0;
|
|
83
|
+
this.bytes = 0;
|
|
84
|
+
try {
|
|
85
|
+
if (reason === "complete")
|
|
86
|
+
this.controller.close();
|
|
87
|
+
else if (reason !== "cancel")
|
|
88
|
+
this.controller.error(Error("Event stream budget exhausted; reconnect using the last received cursor."));
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
this.pendingPull?.();
|
|
92
|
+
this.pendingPull = undefined;
|
|
93
|
+
this.capacityWaiter?.(false);
|
|
94
|
+
this.capacityWaiter = undefined;
|
|
95
|
+
this.onClose(reason);
|
|
96
|
+
}
|
|
97
|
+
status() { return { frames: this.queue.length, bytes: this.bytes, oldestAgeMs: this.queue.length ? performance.now() - this.queue[0].at : 0, closed: this.closed }; }
|
|
98
|
+
}
|
|
@@ -8,7 +8,7 @@ import { parseToolMetricTokenCalculation } from "../../shared/tool-call-token-se
|
|
|
8
8
|
import { PiboWebHttpError, readJsonBody, responseJson } from "../../web/http.js";
|
|
9
9
|
import { CHAT_WEB_API_PREFIX } from "./chat-api-routes.js";
|
|
10
10
|
import { normalizeBasePromptMarkdown, normalizeBasePromptMode, normalizeCompactionPromptMarkdown, normalizeCompactionPromptMode, updateChatModelDefaults, } from "./chat-request-normalizers.js";
|
|
11
|
-
import {
|
|
11
|
+
import { pruneTelemetryOlderThanAsync } from "./telemetry-retention-service.js";
|
|
12
12
|
export function chatSettingsRoute(pathname, method) {
|
|
13
13
|
if (pathname === `${CHAT_WEB_API_PREFIX}/model-defaults` && method === "PATCH")
|
|
14
14
|
return { kind: "model-defaults" };
|
|
@@ -70,8 +70,8 @@ export async function handleChatSettingsRoute(input) {
|
|
|
70
70
|
const days = sanitizeTelemetryRetentionDays(body.days);
|
|
71
71
|
if (!days)
|
|
72
72
|
throw new PiboWebHttpError("Invalid telemetry retention days", 400);
|
|
73
|
-
const result =
|
|
74
|
-
if (result.applied)
|
|
73
|
+
const result = await pruneTelemetryOlderThanAsync({ dataStore: input.dataStore, days, apply: body.dryRun !== true, signal: request.signal });
|
|
74
|
+
if (result.applied && result.completed !== false)
|
|
75
75
|
updateTelemetryRetentionLastPrunedAt(new Date().toISOString());
|
|
76
76
|
return responseJson({ telemetryRetention: result });
|
|
77
77
|
}
|
|
@@ -3,13 +3,31 @@ import { roomWorkspaceFromMetadata } from "../types/rooms.js";
|
|
|
3
3
|
import { tracePayloadRefForStoredPayload } from "../trace-v2.js";
|
|
4
4
|
import { qualifiedToolNodeId } from "../../../shared/trace-tool-identity.js";
|
|
5
5
|
const MAX_TRACE_EVENT_HYDRATION_BYTES = 1024 * 1024;
|
|
6
|
-
export function storedPiboEventFromV2Row(row, payloadStore) {
|
|
6
|
+
export function storedPiboEventFromV2Row(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
|
|
7
|
+
if (typeof hydrationBytes !== "number" || !Number.isFinite(hydrationBytes))
|
|
8
|
+
hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES;
|
|
7
9
|
const attributes = parseJsonObject(row.attributes_json);
|
|
8
|
-
|
|
10
|
+
if (hydrationBytes < MAX_TRACE_EVENT_HYDRATION_BYTES && typeof attributes.inlineText === "string" && Buffer.byteLength(attributes.inlineText) > hydrationBytes)
|
|
11
|
+
attributes.inlineText = row.preview_text ?? "";
|
|
12
|
+
const payload = outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore, hydrationBytes));
|
|
9
13
|
if (!payload)
|
|
10
14
|
return undefined;
|
|
15
|
+
const storedPayloadRef = storedPayloadReference(row, attributes, payloadStore);
|
|
16
|
+
const renderSequence = numberAttribute(attributes, "renderSequence") ?? payload.renderSequence;
|
|
17
|
+
return { id: String(row.stream_id), piboSessionId: row.session_id ?? undefined, eventSequence: row.session_sequence ?? undefined, renderSequence, eventId: row.event_id ?? undefined, streamId: row.stream_id, storedPayloadRef, type: row.type, createdAt: row.created_at, payload };
|
|
18
|
+
}
|
|
19
|
+
export function storedChatEventFromV2Row(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
|
|
20
|
+
if (typeof hydrationBytes !== "number" || !Number.isFinite(hydrationBytes))
|
|
21
|
+
hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES;
|
|
22
|
+
const attributes = parseJsonObject(row.attributes_json);
|
|
23
|
+
if (hydrationBytes < MAX_TRACE_EVENT_HYDRATION_BYTES && typeof attributes.inlineText === "string" && Buffer.byteLength(attributes.inlineText) > hydrationBytes)
|
|
24
|
+
attributes.inlineText = row.preview_text ?? "";
|
|
25
|
+
const storedPayloadRef = storedPayloadReference(row, attributes, payloadStore);
|
|
26
|
+
return { storedPayloadRef, streamId: row.stream_id, roomId: row.room_id ?? undefined, piboSessionId: row.session_id ?? undefined, eventId: row.event_id ?? `evt_${row.stream_id}`, eventType: row.type, actorType: actorTypeValue(row.actor_type), actorId: row.actor_id ?? undefined, clientTxnId: typeof attributes.clientTxnId === "string" ? attributes.clientTxnId : undefined, createdAt: row.created_at, retentionClass: retentionClassValue(row.retention_class), payload: (outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore, hydrationBytes)) ?? null) };
|
|
27
|
+
}
|
|
28
|
+
function storedPayloadReference(row, attributes, payloadStore) {
|
|
11
29
|
const payloadIdentity = tracePayloadIdentityForEvent(row, attributes);
|
|
12
|
-
|
|
30
|
+
return row.payload_ref && row.session_id && payloadStore && payloadIdentity
|
|
13
31
|
? tracePayloadRefForStoredPayload({
|
|
14
32
|
payloadStore,
|
|
15
33
|
piboSessionId: row.session_id,
|
|
@@ -17,16 +35,15 @@ export function storedPiboEventFromV2Row(row, payloadStore) {
|
|
|
17
35
|
...payloadIdentity,
|
|
18
36
|
})
|
|
19
37
|
: undefined;
|
|
20
|
-
const renderSequence = numberAttribute(attributes, "renderSequence") ?? payload.renderSequence;
|
|
21
|
-
return { id: String(row.stream_id), piboSessionId: row.session_id ?? undefined, eventSequence: row.session_sequence ?? undefined, renderSequence, eventId: row.event_id ?? undefined, streamId: row.stream_id, storedPayloadRef, type: row.type, createdAt: row.created_at, payload };
|
|
22
|
-
}
|
|
23
|
-
export function storedChatEventFromV2Row(row, payloadStore) {
|
|
24
|
-
const attributes = parseJsonObject(row.attributes_json);
|
|
25
|
-
return { streamId: row.stream_id, roomId: row.room_id ?? undefined, piboSessionId: row.session_id ?? undefined, eventId: row.event_id ?? `evt_${row.stream_id}`, eventType: row.type, actorType: actorTypeValue(row.actor_type), actorId: row.actor_id ?? undefined, clientTxnId: typeof attributes.clientTxnId === "string" ? attributes.clientTxnId : undefined, createdAt: row.created_at, retentionClass: retentionClassValue(row.retention_class), payload: (outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore)) ?? null) };
|
|
26
38
|
}
|
|
27
39
|
function outputPayloadFromV2Row(row, attributes, persistedPayload) {
|
|
28
40
|
const inlinePayload = attributes.inlinePayload ?? persistedPayload;
|
|
29
|
-
if (inlinePayload
|
|
41
|
+
if (inlinePayload
|
|
42
|
+
&& typeof inlinePayload === "object"
|
|
43
|
+
&& !Array.isArray(inlinePayload)
|
|
44
|
+
&& inlinePayload.type === row.type
|
|
45
|
+
&& typeof inlinePayload.piboSessionId === "string"
|
|
46
|
+
&& inlinePayload.piboSessionId === row.session_id) {
|
|
30
47
|
return compactObject({
|
|
31
48
|
...inlinePayload,
|
|
32
49
|
renderSequence: numberAttribute(attributes, "renderSequence") ?? inlinePayload.renderSequence,
|
|
@@ -73,6 +90,8 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
|
|
|
73
90
|
...base,
|
|
74
91
|
type: "assistant_usage",
|
|
75
92
|
usageIndex: numberAttribute(attributes, "usageIndex"),
|
|
93
|
+
inferenceId: stringAttribute(attributes, "inferenceId"),
|
|
94
|
+
inferenceTarget: attributes.inferenceTarget,
|
|
76
95
|
inputTokens: numberAttribute(attributes, "inputTokens"),
|
|
77
96
|
outputTokens: numberAttribute(attributes, "outputTokens"),
|
|
78
97
|
cacheReadTokens: numberAttribute(attributes, "cacheReadTokens"),
|
|
@@ -112,7 +131,7 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
|
|
|
112
131
|
if (row.type === "compaction_start")
|
|
113
132
|
return { ...base, type: "compaction_start", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown" };
|
|
114
133
|
if (row.type === "compaction_end")
|
|
115
|
-
return { ...base, type: "compaction_end", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown", result: inlinePayload, aborted: booleanAttribute(attributes, "aborted") ?? false, errorMessage: stringAttribute(attributes, "errorMessage") };
|
|
134
|
+
return { ...base, type: "compaction_end", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown", result: inlinePayload, aborted: booleanAttribute(attributes, "aborted") ?? false, errorMessage: stringAttribute(attributes, "errorMessage"), compactionStats: isRecord(attributes.compactionStats) ? attributes.compactionStats : undefined };
|
|
116
135
|
if (row.type === "session_error") {
|
|
117
136
|
const error = stringAttribute(attributes, "error") ?? row.preview_text ?? "Error";
|
|
118
137
|
return { ...base, type: "session_error", error, errorDetails: normalizeSessionErrorDetails(error, isRecord(attributes.errorDetails) ? attributes.errorDetails : undefined) };
|
|
@@ -121,14 +140,14 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
|
|
|
121
140
|
return { type: "user.message.accepted", piboSessionId, roomId: row.room_id ?? undefined, text: stringAttribute(attributes, "inlineText") ?? row.preview_text ?? "", clientTxnId: stringAttribute(attributes, "clientTxnId") };
|
|
122
141
|
return { ...base, type: row.type };
|
|
123
142
|
}
|
|
124
|
-
function readPersistedPayload(row, payloadStore) {
|
|
143
|
+
function readPersistedPayload(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
|
|
125
144
|
if (!row.payload_ref || !payloadStore)
|
|
126
145
|
return undefined;
|
|
127
146
|
try {
|
|
128
147
|
const metadata = payloadStore.getPayload(row.payload_ref);
|
|
129
|
-
if (!metadata || metadata.byteSize >
|
|
148
|
+
if (!metadata || metadata.byteSize > hydrationBytes)
|
|
130
149
|
return undefined;
|
|
131
|
-
const text = Buffer.from(payloadStore.readPayloadBytesBounded(row.payload_ref,
|
|
150
|
+
const text = Buffer.from(payloadStore.readPayloadBytesBounded(row.payload_ref, hydrationBytes)).toString("utf8");
|
|
132
151
|
return metadata.contentType.includes("json") ? JSON.parse(text) : text;
|
|
133
152
|
}
|
|
134
153
|
catch {
|
|
@@ -136,6 +155,13 @@ function readPersistedPayload(row, payloadStore) {
|
|
|
136
155
|
}
|
|
137
156
|
}
|
|
138
157
|
function tracePayloadIdentityForEvent(row, attributes) {
|
|
158
|
+
const semanticId = stringAttribute(attributes, "semanticEventId") ?? row.event_id;
|
|
159
|
+
if (semanticId && (row.type === "assistant_message" || row.type === "thinking_finished")) {
|
|
160
|
+
const thinking = row.type === "thinking_finished";
|
|
161
|
+
const index = numberAttribute(attributes, thinking ? "thinkingIndex" : "assistantIndex") ?? numberAttribute(attributes, "contentIndex");
|
|
162
|
+
const identity = index === undefined ? semanticId : `${semanticId}:${thinking ? "thinking" : "assistant"}:${index}`;
|
|
163
|
+
return { nodeId: `${thinking ? "reasoning" : "assistant"}:${identity}`, payloadKind: thinking ? "reasoning" : "output" };
|
|
164
|
+
}
|
|
139
165
|
const toolCallId = stringAttribute(attributes, "toolCallId");
|
|
140
166
|
if (!toolCallId)
|
|
141
167
|
return undefined;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { isLiveOnlyOutputEvent } from "../output-event-policy.js";
|
|
3
3
|
import { compactObject, nextSessionSequence, previewForPayload, storedChatEventFromV2Row } from "./chat-data-mappers.js";
|
|
4
|
+
export function chatClientTransactionKey(roomId, actorId, clientTxnId) {
|
|
5
|
+
return `chat:user.accepted:${roomId ?? ""}:${actorId ?? ""}:${clientTxnId}`;
|
|
6
|
+
}
|
|
4
7
|
export class ChatEventCommandService {
|
|
5
8
|
store;
|
|
6
9
|
constructor(store) {
|
|
@@ -9,10 +12,7 @@ export class ChatEventCommandService {
|
|
|
9
12
|
appendEvent(input) {
|
|
10
13
|
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
11
14
|
const eventId = input.eventId ?? `ce_${randomUUID()}`;
|
|
12
|
-
const idempotencyKey = input.clientTxnId ?
|
|
13
|
-
const existing = input.clientTxnId ? this.findByClientTxn(input.roomId, input.actorId, input.clientTxnId) : undefined;
|
|
14
|
-
if (existing)
|
|
15
|
-
return existing;
|
|
15
|
+
const idempotencyKey = input.clientTxnId ? chatClientTransactionKey(input.roomId, input.actorId, input.clientTxnId) : `chat:event:${eventId}`;
|
|
16
16
|
const stored = this.store.eventLog.appendEvent({
|
|
17
17
|
sessionId: input.piboSessionId,
|
|
18
18
|
sessionSequence: input.piboSessionId ? nextSessionSequence(this.store, input.piboSessionId) : undefined,
|
|
@@ -30,21 +30,7 @@ export class ChatEventCommandService {
|
|
|
30
30
|
createdAt,
|
|
31
31
|
indexedAt: createdAt,
|
|
32
32
|
});
|
|
33
|
-
return
|
|
34
|
-
stream_id: stored.streamId,
|
|
35
|
-
session_id: stored.sessionId ?? null,
|
|
36
|
-
session_sequence: stored.sessionSequence ?? null,
|
|
37
|
-
room_id: stored.roomId ?? null,
|
|
38
|
-
type: stored.type,
|
|
39
|
-
actor_type: stored.actorType ?? null,
|
|
40
|
-
actor_id: stored.actorId ?? null,
|
|
41
|
-
event_id: stored.eventId ?? null,
|
|
42
|
-
idempotency_key: stored.idempotencyKey ?? null,
|
|
43
|
-
retention_class: stored.retentionClass,
|
|
44
|
-
preview_text: stored.previewText ?? null,
|
|
45
|
-
attributes_json: JSON.stringify(stored.attributes),
|
|
46
|
-
created_at: stored.createdAt,
|
|
47
|
-
});
|
|
33
|
+
return chatEventFromStoredRow(stored);
|
|
48
34
|
}
|
|
49
35
|
appendOutputEvent(event, _input = {}) {
|
|
50
36
|
// Runtime output ingestion is owned by ChatDataIngestService. Avoid a second event_log insert here.
|
|
@@ -55,8 +41,14 @@ export class ChatEventCommandService {
|
|
|
55
41
|
findByClientTxn(roomId, actorId, clientTxnId) {
|
|
56
42
|
if (!roomId || !actorId)
|
|
57
43
|
return undefined;
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
// Empty IDs are not valid HTTP transaction keys. Retain the legacy direct-call
|
|
45
|
+
// lookup contract without adding a scan fallback to ordinary indexed misses.
|
|
46
|
+
if (clientTxnId === "") {
|
|
47
|
+
const row = this.store.db.prepare("SELECT * FROM event_log WHERE room_id = ? AND actor_id = ? AND json_extract(attributes_json, '$.clientTxnId') = ? ORDER BY stream_id ASC LIMIT 1").get(roomId, actorId, clientTxnId);
|
|
48
|
+
return row ? storedChatEventFromV2Row(row) : undefined;
|
|
49
|
+
}
|
|
50
|
+
const row = this.store.eventLog.findByIdempotencyKey(chatClientTransactionKey(roomId, actorId, clientTxnId));
|
|
51
|
+
return row ? chatEventFromStoredRow(row) : undefined;
|
|
60
52
|
}
|
|
61
53
|
deleteSessions(piboSessionIds) {
|
|
62
54
|
if (!piboSessionIds.length)
|
|
@@ -73,3 +65,20 @@ export class ChatEventCommandService {
|
|
|
73
65
|
return Number(result.changes ?? 0);
|
|
74
66
|
}
|
|
75
67
|
}
|
|
68
|
+
function chatEventFromStoredRow(stored) {
|
|
69
|
+
return storedChatEventFromV2Row({
|
|
70
|
+
stream_id: stored.streamId,
|
|
71
|
+
session_id: stored.sessionId ?? null,
|
|
72
|
+
session_sequence: stored.sessionSequence ?? null,
|
|
73
|
+
room_id: stored.roomId ?? null,
|
|
74
|
+
type: stored.type,
|
|
75
|
+
actor_type: stored.actorType ?? null,
|
|
76
|
+
actor_id: stored.actorId ?? null,
|
|
77
|
+
event_id: stored.eventId ?? null,
|
|
78
|
+
idempotency_key: stored.idempotencyKey ?? null,
|
|
79
|
+
retention_class: stored.retentionClass,
|
|
80
|
+
preview_text: stored.previewText ?? null,
|
|
81
|
+
attributes_json: JSON.stringify(stored.attributes),
|
|
82
|
+
created_at: stored.createdAt,
|
|
83
|
+
});
|
|
84
|
+
}
|