@pasko70/pibo 3.5.0 → 3.6.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 +80 -19
- package/dist/agent-runtimes/codex-native/turn.js +27 -3
- package/dist/apps/chat/data/chat-data-mappers.js +8 -1
- package/dist/apps/chat/data/read-state-service.js +24 -3
- package/dist/apps/chat/message-command-dispatcher.js +16 -1
- package/dist/apps/chat/web-app.js +40 -19
- package/dist/apps/chat-ui/assets/{dist-BG0n7zLd.js → dist-BQ-TQZet.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D79vyxSX.js → dist-CYTRhePp.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D6TjFhAm.js → dist-Crj0LNXs.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DFZ8cwh0.js → dist-t24MVWWG.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-cOjokPrK.js → dist-tFj2S0WU.js} +1 -1
- package/dist/apps/chat-ui/assets/index-Bc9O0z52.css +1 -0
- package/dist/apps/chat-ui/assets/{index-RMHUTJ62.js → index-Cr2oFRhI.js} +77 -77
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/{index-xacbCyTx.js → index-DgKYVP-6.js} +11 -11
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/cli-session/localSessionSource.js +3 -2
- package/dist/core/output-render-sequence.js +63 -6
- package/dist/core/session-router.js +99 -11
- package/dist/data/async-chat-storage.js +7 -2
- package/dist/data/bounded-worker-client.js +1 -1
- package/dist/data/chat-read-projections.js +4 -4
- package/dist/data/chat-storage-worker.js +24 -3
- package/dist/data/ingest-service.js +73 -4
- package/dist/data/message-command-store.js +153 -11
- package/dist/data/schema.js +24 -3
- package/dist/data/storage-maintenance.js +344 -0
- package/dist/data/storage-verification-worker.js +25 -0
- package/dist/debug/cache.js +136 -0
- package/dist/debug/index.js +201 -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/session.js +2 -0
- package/dist/debug/storage-backup.js +12 -4
- package/dist/debug/storage-maintenance.js +78 -0
- package/dist/debug/summary.js +1 -0
- package/dist/debug/trace.js +28 -0
- package/dist/gateway/cli.js +71 -7
- package/dist/gateway/server.js +1 -0
- package/dist/reliability/store.js +119 -23
- package/dist/session-ui/terminalRows.js +7 -8
- package/dist/sessions/pibo-data-store.js +18 -14
- package/dist/shared/cache-observability.js +73 -0
- package/dist/shared/model-inference-metrics.js +20 -7
- package/dist/shared/trace-event-projection.js +44 -6
- package/dist/web/channel.js +114 -12
- package/dist/web/http.js +105 -44
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-hEkrlRk-.css +0 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<meta name="theme-color" content="#101d22" />
|
|
7
7
|
<title>Pibo</title>
|
|
8
|
-
<script type="module" crossorigin src="/apps/chat-vscode/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/apps/chat-vscode/assets/index-DgKYVP-6.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-b18ZkEo0.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -641,7 +641,7 @@ export class LocalCliSessionSource {
|
|
|
641
641
|
if (delivery.persisted)
|
|
642
642
|
continue;
|
|
643
643
|
try {
|
|
644
|
-
this.ingestOutputEvent(session, delivery.event);
|
|
644
|
+
this.ingestOutputEvent(session, delivery.event, retryContext.attempt > 1 ? "durable-replay" : "live");
|
|
645
645
|
}
|
|
646
646
|
catch (error) {
|
|
647
647
|
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -753,7 +753,7 @@ export class LocalCliSessionSource {
|
|
|
753
753
|
// Persistence is best-effort; live local rendering still proceeds through router events.
|
|
754
754
|
}
|
|
755
755
|
}
|
|
756
|
-
ingestOutputEvent(session, event) {
|
|
756
|
+
ingestOutputEvent(session, event, phase) {
|
|
757
757
|
if (!this.ingestService)
|
|
758
758
|
return;
|
|
759
759
|
this.ingestService.ingestOutputEvent({
|
|
@@ -762,6 +762,7 @@ export class LocalCliSessionSource {
|
|
|
762
762
|
actorId: session.profile,
|
|
763
763
|
event,
|
|
764
764
|
createdAt: this.now(),
|
|
765
|
+
persistenceProvenance: { producer: "local-cli", projection: "product-history", phase },
|
|
765
766
|
});
|
|
766
767
|
}
|
|
767
768
|
buildStatus(session) {
|
|
@@ -109,9 +109,10 @@ export class OutputRenderSequencer {
|
|
|
109
109
|
const suppliedPart = transition.suppliedIndex === undefined
|
|
110
110
|
? undefined
|
|
111
111
|
: parts.find((part) => part.index === transition.suppliedIndex);
|
|
112
|
-
if (transition.suppliedIndex !== undefined && (transition.canonicalIndex
|
|
112
|
+
if (transition.suppliedIndex !== undefined && (transition.canonicalIndex
|
|
113
|
+
|| (suppliedPart && (!suppliedPart.closed || suppliedPart.identityFingerprint === transition.identityFingerprint)))) {
|
|
113
114
|
this.highWaterStore?.observeOutputPart?.({ ...transition, index: transition.suppliedIndex });
|
|
114
|
-
this.recordOutputPart(parts, transition.suppliedIndex, transition.terminal);
|
|
115
|
+
this.recordOutputPart(parts, transition.suppliedIndex, transition.terminal, transition.identityFingerprint);
|
|
115
116
|
this.trimOutputParts(state);
|
|
116
117
|
return event;
|
|
117
118
|
}
|
|
@@ -122,18 +123,20 @@ export class OutputRenderSequencer {
|
|
|
122
123
|
const index = latestOpen
|
|
123
124
|
? localIndex
|
|
124
125
|
: this.highWaterStore?.claimOrAttachOutputPart?.({ ...transition, proposedIndex: localIndex }) ?? localIndex;
|
|
125
|
-
this.recordOutputPart(parts, index, transition.terminal);
|
|
126
|
+
this.recordOutputPart(parts, index, transition.terminal, transition.identityFingerprint);
|
|
126
127
|
this.trimOutputParts(state);
|
|
127
128
|
return withOutputPartIndex(event, transition.kind, index);
|
|
128
129
|
}
|
|
129
|
-
recordOutputPart(parts, index, terminal) {
|
|
130
|
+
recordOutputPart(parts, index, terminal, identityFingerprint) {
|
|
130
131
|
let part = parts.find((candidate) => candidate.index === index);
|
|
131
132
|
if (!part) {
|
|
132
|
-
part = { index, closed: false };
|
|
133
|
+
part = { index, closed: false, identityFingerprint };
|
|
133
134
|
parts.push(part);
|
|
134
135
|
}
|
|
135
|
-
if (terminal)
|
|
136
|
+
if (terminal) {
|
|
136
137
|
part.closed = true;
|
|
138
|
+
part.identityFingerprint = identityFingerprint;
|
|
139
|
+
}
|
|
137
140
|
}
|
|
138
141
|
trimOutputParts(state) {
|
|
139
142
|
let total = 0;
|
|
@@ -380,12 +383,66 @@ function outputPartTransition(event, activeEventId) {
|
|
|
380
383
|
function validOutputPartIndex(value) {
|
|
381
384
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
382
385
|
}
|
|
386
|
+
export const OUTPUT_IDENTITY_FINGERPRINT_VERSION = 2;
|
|
383
387
|
export function outputIdentityFingerprint(event) {
|
|
388
|
+
return createHash("sha256").update(stableJson(outputIdentityPayload(event))).digest("hex");
|
|
389
|
+
}
|
|
390
|
+
/** Exact v1 algorithm retained only for comparing pre-v2 persisted fingerprints. */
|
|
391
|
+
export function legacyOutputIdentityFingerprint(event) {
|
|
384
392
|
const payload = { ...event };
|
|
385
393
|
delete payload.renderSequence;
|
|
386
394
|
delete payload.compactionStats;
|
|
387
395
|
return createHash("sha256").update(stableJson(payload)).digest("hex");
|
|
388
396
|
}
|
|
397
|
+
export function legacyOutputIdentityFingerprintCandidates(event) {
|
|
398
|
+
const variants = [{ ...event }];
|
|
399
|
+
const assistant = event.type === "assistant_delta" || event.type === "assistant_message";
|
|
400
|
+
const thinking = event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished";
|
|
401
|
+
const index = assistant ? event.assistantIndex ?? event.contentIndex : thinking ? event.thinkingIndex ?? event.contentIndex : undefined;
|
|
402
|
+
if (index !== undefined && (assistant || thinking)) {
|
|
403
|
+
for (const attribute of assistant ? ["assistantIndex", "contentIndex"] : ["thinkingIndex", "contentIndex"]) {
|
|
404
|
+
const variant = { ...event };
|
|
405
|
+
delete variant.assistantIndex;
|
|
406
|
+
delete variant.thinkingIndex;
|
|
407
|
+
delete variant.contentIndex;
|
|
408
|
+
variant[attribute] = index;
|
|
409
|
+
variants.push(variant);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
for (const variant of [...variants]) {
|
|
413
|
+
if ("provenance" in variant) {
|
|
414
|
+
const withoutProvenance = { ...variant };
|
|
415
|
+
delete withoutProvenance.provenance;
|
|
416
|
+
variants.push(withoutProvenance);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return [...new Set(variants.map((variant) => legacyOutputIdentityFingerprint(variant)))];
|
|
420
|
+
}
|
|
421
|
+
/** Redacted, bounded evidence for diagnosing a fingerprint mismatch without retaining values. */
|
|
422
|
+
export function outputIdentityFieldDigests(event) {
|
|
423
|
+
return Object.fromEntries(Object.entries(outputIdentityPayload(event))
|
|
424
|
+
.slice(0, 64)
|
|
425
|
+
.map(([field, value]) => [field, createHash("sha256").update(stableJson(value)).digest("hex").slice(0, 16)]));
|
|
426
|
+
}
|
|
427
|
+
function outputIdentityPayload(event) {
|
|
428
|
+
const payload = { ...event };
|
|
429
|
+
delete payload.renderSequence;
|
|
430
|
+
delete payload.compactionStats;
|
|
431
|
+
// Delivery provenance describes how equivalent output reached persistence; it
|
|
432
|
+
// is diagnostic metadata rather than semantic message content.
|
|
433
|
+
delete payload.provenance;
|
|
434
|
+
if (event.type === "assistant_delta" || event.type === "assistant_message") {
|
|
435
|
+
delete payload.assistantIndex;
|
|
436
|
+
delete payload.contentIndex;
|
|
437
|
+
payload.outputPartIndex = event.assistantIndex ?? event.contentIndex ?? 0;
|
|
438
|
+
}
|
|
439
|
+
else if (event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished") {
|
|
440
|
+
delete payload.thinkingIndex;
|
|
441
|
+
delete payload.contentIndex;
|
|
442
|
+
payload.outputPartIndex = event.thinkingIndex ?? event.contentIndex ?? 0;
|
|
443
|
+
}
|
|
444
|
+
return payload;
|
|
445
|
+
}
|
|
389
446
|
export function outputPartFingerprint(event) {
|
|
390
447
|
const payload = { ...event };
|
|
391
448
|
delete payload.renderSequence;
|
|
@@ -175,6 +175,39 @@ function formatRunReminderMessage(notification) {
|
|
|
175
175
|
function isRunReminderServiceMessage(event) {
|
|
176
176
|
return event.source === "service" && event.text.startsWith("<pibo_run_notification>");
|
|
177
177
|
}
|
|
178
|
+
const RUNTIME_QUEUE_CAPACITY_DIMENSIONS = new Set([
|
|
179
|
+
"message_bytes",
|
|
180
|
+
"queue_count",
|
|
181
|
+
"queue_bytes",
|
|
182
|
+
"oldest_wait_age",
|
|
183
|
+
]);
|
|
184
|
+
function runReminderAdmissionDiagnostic(error) {
|
|
185
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "runtime_capacity_unavailable") {
|
|
186
|
+
return {
|
|
187
|
+
code: "run_reminder_delivery_unavailable",
|
|
188
|
+
warning: "Run reminder deferred: internal delivery is temporarily unavailable; pending run state is retained.",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const candidate = error;
|
|
192
|
+
const dimension = typeof candidate.dimension === "string"
|
|
193
|
+
&& RUNTIME_QUEUE_CAPACITY_DIMENSIONS.has(candidate.dimension)
|
|
194
|
+
? candidate.dimension
|
|
195
|
+
: undefined;
|
|
196
|
+
const current = candidate.current
|
|
197
|
+
&& ["messageBytes", "queueCount", "queueBytes", "oldestWaitMs"].every((key) => (typeof candidate.current?.[key] === "number" && Number.isFinite(candidate.current[key]) && candidate.current[key] >= 0))
|
|
198
|
+
? candidate.current
|
|
199
|
+
: undefined;
|
|
200
|
+
const limit = typeof candidate.limit === "number" && Number.isFinite(candidate.limit) && candidate.limit >= 0
|
|
201
|
+
? candidate.limit
|
|
202
|
+
: undefined;
|
|
203
|
+
return {
|
|
204
|
+
code: "runtime_capacity_unavailable",
|
|
205
|
+
warning: `Run reminder deferred: runtime queue${dimension ? ` ${dimension}` : ""} capacity is unavailable; pending run state is retained.`,
|
|
206
|
+
...(dimension ? { dimension } : {}),
|
|
207
|
+
...(current ? { current } : {}),
|
|
208
|
+
...(limit !== undefined ? { limit } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
178
211
|
function isRunReminderContextPressureError(event) {
|
|
179
212
|
return event.errorDetails?.category === "context_overflow"
|
|
180
213
|
|| event.errorDetails?.code === "context_length_exceeded"
|
|
@@ -365,6 +398,7 @@ export class PiboSessionRouter {
|
|
|
365
398
|
deferredRunReminders = new Map();
|
|
366
399
|
runReminderRecoveries = new Map();
|
|
367
400
|
runReminderGenerations = new Map();
|
|
401
|
+
runReminderAdmissionWarnings = new Map();
|
|
368
402
|
runCancellationHandlers = new Map();
|
|
369
403
|
activeRunExecutions = new Set();
|
|
370
404
|
quiescingSessions = new Set();
|
|
@@ -1038,6 +1072,13 @@ export class PiboSessionRouter {
|
|
|
1038
1072
|
listRuns(options = {}) {
|
|
1039
1073
|
return this.runRegistry.listAll(options);
|
|
1040
1074
|
}
|
|
1075
|
+
getRunJobReliabilityStatus() {
|
|
1076
|
+
return this.reliabilityStore?.getRunJobReliabilityStatus() ?? {
|
|
1077
|
+
status: "ok",
|
|
1078
|
+
expiredOrphanRunJobs: 0,
|
|
1079
|
+
orphanRunDeadLetters: 0,
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1041
1082
|
getSignalRegistry() {
|
|
1042
1083
|
return this.signalRegistry;
|
|
1043
1084
|
}
|
|
@@ -1630,6 +1671,7 @@ export class PiboSessionRouter {
|
|
|
1630
1671
|
},
|
|
1631
1672
|
statusResources,
|
|
1632
1673
|
getToolMetricTokenCalculation: () => loadPiboUserSettings().toolMetrics.tokenCalculation,
|
|
1674
|
+
now: this.options.runtimeQueueNow,
|
|
1633
1675
|
});
|
|
1634
1676
|
this.sessions.set(piboSession.id, session);
|
|
1635
1677
|
return session;
|
|
@@ -2661,6 +2703,7 @@ export class PiboSessionRouter {
|
|
|
2661
2703
|
this.runRegistry.suppressNotification(delivery.piboSessionId, run.runId);
|
|
2662
2704
|
this.clearRunReminderRecovery(delivery);
|
|
2663
2705
|
this.deferredRunReminders.delete(delivery.piboSessionId);
|
|
2706
|
+
this.clearRunReminderAdmissionWarning(delivery.piboSessionId, delivery.generation);
|
|
2664
2707
|
this.sessions.get(delivery.piboSessionId)?.removeQueuedMessages(isRunReminderServiceMessage);
|
|
2665
2708
|
this.scheduleRunReminder(delivery.piboSessionId, false, delivery.generation);
|
|
2666
2709
|
}
|
|
@@ -2750,6 +2793,7 @@ export class PiboSessionRouter {
|
|
|
2750
2793
|
this.scheduledRunReminders.delete(piboSessionId);
|
|
2751
2794
|
this.deferredRunReminders.delete(piboSessionId);
|
|
2752
2795
|
this.runReminderRecoveries.delete(piboSessionId);
|
|
2796
|
+
this.clearRunReminderAdmissionWarning(piboSessionId);
|
|
2753
2797
|
for (const [eventId, delivery] of this.runReminderDeliveries) {
|
|
2754
2798
|
if (delivery.piboSessionId === piboSessionId)
|
|
2755
2799
|
this.runReminderDeliveries.delete(eventId);
|
|
@@ -2787,8 +2831,18 @@ export class PiboSessionRouter {
|
|
|
2787
2831
|
return;
|
|
2788
2832
|
if (deferredGeneration !== undefined)
|
|
2789
2833
|
this.deferredRunReminders.delete(piboSessionId);
|
|
2790
|
-
if (!this.runRegistry.hasPendingNotification(piboSessionId, { includeAlreadyNotified }))
|
|
2834
|
+
if (!this.runRegistry.hasPendingNotification(piboSessionId, { includeAlreadyNotified })) {
|
|
2835
|
+
this.clearRunReminderAdmissionWarning(piboSessionId, expectedGeneration);
|
|
2791
2836
|
return;
|
|
2837
|
+
}
|
|
2838
|
+
if (includeAlreadyNotified) {
|
|
2839
|
+
const status = this.sessions.get(piboSessionId)?.getStatus?.();
|
|
2840
|
+
const effectiveEventIds = new Set([status?.activeEventId, ...(status?.queuedEventIds ?? [])].filter((id) => Boolean(id)));
|
|
2841
|
+
if ([...this.runReminderDeliveries.entries()].some(([eventId, delivery]) => (effectiveEventIds.has(eventId)
|
|
2842
|
+
&& delivery.piboSessionId === piboSessionId
|
|
2843
|
+
&& delivery.generation === expectedGeneration)))
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2792
2846
|
const previous = this.scheduledRunReminders.get(piboSessionId);
|
|
2793
2847
|
if (previous?.generation === expectedGeneration) {
|
|
2794
2848
|
this.scheduledRunReminders.set(piboSessionId, {
|
|
@@ -2804,8 +2858,33 @@ export class PiboSessionRouter {
|
|
|
2804
2858
|
}
|
|
2805
2859
|
refreshQueuedRunReminders(piboSessionId) {
|
|
2806
2860
|
const removed = this.sessions.get(piboSessionId)?.removeQueuedMessages(isRunReminderServiceMessage) ?? 0;
|
|
2807
|
-
if (removed > 0)
|
|
2861
|
+
if (removed > 0) {
|
|
2808
2862
|
this.scheduleRunReminder(piboSessionId, true);
|
|
2863
|
+
}
|
|
2864
|
+
else if (!this.runRegistry.hasPendingNotification(piboSessionId, { includeAlreadyNotified: true })) {
|
|
2865
|
+
this.clearRunReminderAdmissionWarning(piboSessionId);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
clearRunReminderAdmissionWarning(piboSessionId, generation) {
|
|
2869
|
+
if (generation !== undefined && this.runReminderAdmissionWarnings.get(piboSessionId) !== generation)
|
|
2870
|
+
return;
|
|
2871
|
+
this.runReminderAdmissionWarnings.delete(piboSessionId);
|
|
2872
|
+
this.sessions.get(piboSessionId)?.setRunReminderDeferredWarning?.(undefined);
|
|
2873
|
+
}
|
|
2874
|
+
deferRunReminderAdmission(piboSessionId, generation, error) {
|
|
2875
|
+
const diagnostic = runReminderAdmissionDiagnostic(error);
|
|
2876
|
+
this.sessions.get(piboSessionId)?.setRunReminderDeferredWarning?.(diagnostic.warning);
|
|
2877
|
+
if (this.runReminderAdmissionWarnings.get(piboSessionId) === generation)
|
|
2878
|
+
return;
|
|
2879
|
+
this.runReminderAdmissionWarnings.set(piboSessionId, generation);
|
|
2880
|
+
console.warn("[pibo] run reminder deferred", {
|
|
2881
|
+
piboSessionId,
|
|
2882
|
+
generation,
|
|
2883
|
+
code: diagnostic.code,
|
|
2884
|
+
...(diagnostic.dimension ? { dimension: diagnostic.dimension } : {}),
|
|
2885
|
+
...(diagnostic.current ? { current: diagnostic.current } : {}),
|
|
2886
|
+
...(diagnostic.limit !== undefined ? { limit: diagnostic.limit } : {}),
|
|
2887
|
+
});
|
|
2809
2888
|
}
|
|
2810
2889
|
async deliverRunReminder(piboSessionId, expectedGeneration) {
|
|
2811
2890
|
const scheduled = this.scheduledRunReminders.get(piboSessionId);
|
|
@@ -2820,9 +2899,21 @@ export class PiboSessionRouter {
|
|
|
2820
2899
|
const session = await this.getOrCreateSession(piboSessionId);
|
|
2821
2900
|
if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
|
|
2822
2901
|
return;
|
|
2823
|
-
|
|
2824
|
-
|
|
2902
|
+
// Release the queued snapshot before reserving its replacement. The
|
|
2903
|
+
// interruption callback restores every unconsumed run to pending state.
|
|
2904
|
+
const replaced = session.removeQueuedMessages?.((event) => {
|
|
2905
|
+
if (!isRunReminderServiceMessage(event) || !event.id)
|
|
2906
|
+
return false;
|
|
2907
|
+
const delivery = this.runReminderDeliveries.get(event.id);
|
|
2908
|
+
return delivery?.piboSessionId === piboSessionId && delivery.generation === expectedGeneration;
|
|
2909
|
+
}) ?? 0;
|
|
2910
|
+
notification = this.runRegistry.createNotification(piboSessionId, {
|
|
2911
|
+
includeAlreadyNotified: scheduled.includeAlreadyNotified || replaced > 0,
|
|
2912
|
+
});
|
|
2913
|
+
if (!notification) {
|
|
2914
|
+
this.clearRunReminderAdmissionWarning(piboSessionId, expectedGeneration);
|
|
2825
2915
|
return;
|
|
2916
|
+
}
|
|
2826
2917
|
eventId = randomUUID();
|
|
2827
2918
|
this.runReminderDeliveries.set(eventId, { piboSessionId, generation: expectedGeneration, notification });
|
|
2828
2919
|
session.enqueueMessage({
|
|
@@ -2833,6 +2924,7 @@ export class PiboSessionRouter {
|
|
|
2833
2924
|
id: eventId,
|
|
2834
2925
|
provenance: runReminderProvenance(notification),
|
|
2835
2926
|
});
|
|
2927
|
+
this.clearRunReminderAdmissionWarning(piboSessionId, expectedGeneration);
|
|
2836
2928
|
}
|
|
2837
2929
|
catch (error) {
|
|
2838
2930
|
if (eventId)
|
|
@@ -2841,13 +2933,9 @@ export class PiboSessionRouter {
|
|
|
2841
2933
|
this.runRegistry.releaseNotification(piboSessionId, notification);
|
|
2842
2934
|
if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
|
|
2843
2935
|
return;
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
piboSessionId,
|
|
2848
|
-
error: message,
|
|
2849
|
-
errorDetails: runtimeSessionErrorDetails(message),
|
|
2850
|
-
});
|
|
2936
|
+
// Reminder delivery is internal recovery work. Preserve it for the next
|
|
2937
|
+
// bounded state transition instead of emitting an unscoped fatal error.
|
|
2938
|
+
this.deferRunReminderAdmission(piboSessionId, expectedGeneration, error);
|
|
2851
2939
|
}
|
|
2852
2940
|
}
|
|
2853
2941
|
}
|
|
@@ -69,8 +69,12 @@ export class AsyncChatStorage {
|
|
|
69
69
|
ingestUser(input) {
|
|
70
70
|
return this.writer.request({ type: "ingestUser", input });
|
|
71
71
|
}
|
|
72
|
-
ingestOutput(input) {
|
|
73
|
-
|
|
72
|
+
async ingestOutput(input) {
|
|
73
|
+
const result = await this.writer.request({ type: "ingestOutput", input }, { priority: "output", fairnessKey: input.roomId });
|
|
74
|
+
if (input.event.type === "compaction_end" && result.enrichment?.compactionStats) {
|
|
75
|
+
input.event.compactionStats = result.enrichment.compactionStats;
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
74
78
|
}
|
|
75
79
|
cancelPendingCommands(sessionId) { return this.writer.request({ type: "cancelPendingCommands", sessionId }, { priority: "control" }); }
|
|
76
80
|
commandReceiptPage(sessionId) { return this.writer.request({ type: "commandReceiptPage", sessionId }, { priority: "control" }); }
|
|
@@ -79,6 +83,7 @@ export class AsyncChatStorage {
|
|
|
79
83
|
claimCommand(owner, leaseMs) { return this.writer.request({ type: "claimCommand", owner, leaseMs }, { priority: "background" }); }
|
|
80
84
|
transitionCommand(id, owner, token, state, error) { return this.writer.request({ type: "transitionCommand", id, owner, token, state, error }, { priority: "control" }); }
|
|
81
85
|
heartbeatCommand(id, owner, token, leaseMs) { return this.writer.request({ type: "heartbeatCommand", id, owner, token, leaseMs }, { priority: "control" }); }
|
|
86
|
+
durableQueueHealth() { return this.writer.request({ type: "durableQueueHealth" }, { priority: "control", timeoutMs: 500 }); }
|
|
82
87
|
status() {
|
|
83
88
|
const writer = this.writer.status();
|
|
84
89
|
const reader = this.reader?.status();
|
|
@@ -112,7 +112,7 @@ export class BoundedWorkerClient {
|
|
|
112
112
|
pending.settled = true;
|
|
113
113
|
if (message.error) {
|
|
114
114
|
this.rejected++;
|
|
115
|
-
pending.reject(Object.assign(new Error(message.error.message ?? "Storage operation failed."), { code: message.error.code ?? "storage_operation_failed" }));
|
|
115
|
+
pending.reject(Object.assign(new Error(message.error.message ?? "Storage operation failed."), { code: message.error.code ?? "storage_operation_failed", ...message.error.details }));
|
|
116
116
|
}
|
|
117
117
|
else {
|
|
118
118
|
try {
|
|
@@ -12,7 +12,7 @@ END;
|
|
|
12
12
|
CREATE TRIGGER IF NOT EXISTS chat_trace_delete AFTER DELETE ON event_log WHEN OLD.session_id IS NOT NULL BEGIN
|
|
13
13
|
INSERT INTO chat_trace_revisions VALUES(OLD.session_id,1) ON CONFLICT(session_id) DO UPDATE SET revision=revision+1;
|
|
14
14
|
END;
|
|
15
|
-
CREATE INDEX IF NOT EXISTS idx_event_log_unread_stream ON event_log(stream_id) WHERE
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_event_log_unread_stream ON event_log(stream_id) WHERE type='message_finished';
|
|
16
16
|
CREATE TABLE IF NOT EXISTS chat_unread_index(stream_id INTEGER PRIMARY KEY,session_id TEXT NOT NULL);
|
|
17
17
|
CREATE INDEX IF NOT EXISTS idx_chat_unread_session_stream ON chat_unread_index(session_id,stream_id);
|
|
18
18
|
CREATE TABLE IF NOT EXISTS chat_unread_counts(session_id TEXT PRIMARY KEY,unread_count INTEGER NOT NULL DEFAULT 0);
|
|
@@ -23,13 +23,13 @@ END;
|
|
|
23
23
|
CREATE TRIGGER IF NOT EXISTS chat_unread_count_delete AFTER DELETE ON chat_unread_index BEGIN
|
|
24
24
|
UPDATE chat_unread_counts SET unread_count=MAX(0,unread_count-CASE WHEN OLD.stream_id>COALESCE((SELECT last_read_stream_id FROM app_session_read_state WHERE session_id=OLD.session_id),0) THEN 1 ELSE 0 END) WHERE session_id=OLD.session_id;
|
|
25
25
|
END;
|
|
26
|
-
CREATE TRIGGER IF NOT EXISTS chat_unread_event_insert AFTER INSERT ON event_log WHEN NEW.session_id IS NOT NULL AND
|
|
26
|
+
CREATE TRIGGER IF NOT EXISTS chat_unread_event_insert AFTER INSERT ON event_log WHEN NEW.session_id IS NOT NULL AND NEW.type='message_finished' BEGIN
|
|
27
27
|
INSERT OR IGNORE INTO chat_unread_index VALUES(NEW.stream_id,NEW.session_id);
|
|
28
28
|
END;
|
|
29
29
|
CREATE TRIGGER IF NOT EXISTS chat_unread_event_delete AFTER DELETE ON event_log BEGIN DELETE FROM chat_unread_index WHERE stream_id=OLD.stream_id; END;
|
|
30
30
|
CREATE TRIGGER IF NOT EXISTS chat_unread_event_update AFTER UPDATE ON event_log BEGIN
|
|
31
31
|
DELETE FROM chat_unread_index WHERE stream_id=OLD.stream_id;
|
|
32
|
-
INSERT OR IGNORE INTO chat_unread_index SELECT NEW.stream_id,NEW.session_id WHERE NEW.session_id IS NOT NULL AND
|
|
32
|
+
INSERT OR IGNORE INTO chat_unread_index SELECT NEW.stream_id,NEW.session_id WHERE NEW.session_id IS NOT NULL AND NEW.type='message_finished';
|
|
33
33
|
END;
|
|
34
34
|
CREATE TRIGGER IF NOT EXISTS chat_unread_mark_insert AFTER INSERT ON app_session_read_state BEGIN
|
|
35
35
|
INSERT INTO chat_unread_counts(session_id,unread_count) SELECT NEW.session_id,COUNT(*) FROM chat_unread_index WHERE session_id=NEW.session_id AND stream_id>NEW.last_read_stream_id
|
|
@@ -103,7 +103,7 @@ export class ChatReadProjectionStore {
|
|
|
103
103
|
return { ...current, processed: 0 };
|
|
104
104
|
}
|
|
105
105
|
if (current.historyComplete) {
|
|
106
|
-
const rows = this.db.prepare("SELECT stream_id,session_id FROM event_log INDEXED BY idx_event_log_unread_stream WHERE stream_id>? AND stream_id<=? AND
|
|
106
|
+
const rows = this.db.prepare("SELECT stream_id,session_id FROM event_log INDEXED BY idx_event_log_unread_stream WHERE stream_id>? AND stream_id<=? AND type='message_finished' ORDER BY stream_id LIMIT ?").all(current.event_cursor, current.event_target, limit);
|
|
107
107
|
const insert = this.db.prepare("INSERT OR IGNORE INTO chat_unread_index VALUES(?,?)");
|
|
108
108
|
let processed = 0;
|
|
109
109
|
const started = performance.now();
|
|
@@ -19,6 +19,15 @@ const ingest = new ChatDataIngestService(store);
|
|
|
19
19
|
const rooms = new ChatRoomService(store);
|
|
20
20
|
const sessions = new ChatSessionQueryService(store);
|
|
21
21
|
const messageCommands = new MessageCommandStore(store);
|
|
22
|
+
// Startup repair is bounded and conservative: terminal evidence may settle a receipt,
|
|
23
|
+
// but ambiguous work is retained and never replayed. A damaged row must not stop the worker.
|
|
24
|
+
let startupReconciliation;
|
|
25
|
+
try {
|
|
26
|
+
startupReconciliation = messageCommands.reconcileInterrupted();
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
startupReconciliation = { error: error instanceof Error ? error.message.slice(0, 500) : "Unknown reconciliation error" };
|
|
30
|
+
}
|
|
22
31
|
let active = false;
|
|
23
32
|
let operations = 0;
|
|
24
33
|
let busyRetries = 0;
|
|
@@ -32,6 +41,7 @@ function execute(command) {
|
|
|
32
41
|
case "claimCommand": return messageCommands.claim(command.owner, command.leaseMs);
|
|
33
42
|
case "transitionCommand": return messageCommands.transition(command.id, command.owner, command.token, command.state, command.error);
|
|
34
43
|
case "heartbeatCommand": return messageCommands.heartbeat(command.id, command.owner, command.token, command.leaseMs);
|
|
44
|
+
case "durableQueueHealth": return messageCommands.health();
|
|
35
45
|
case "resolveRoom": {
|
|
36
46
|
const room = command.roomId ? rooms.getRoom(command.roomId) : undefined;
|
|
37
47
|
if (room)
|
|
@@ -54,6 +64,8 @@ function execute(command) {
|
|
|
54
64
|
throw Object.assign(new Error("Transaction belongs to the legacy admission contract."), { code: "command_conflict" });
|
|
55
65
|
if (existing)
|
|
56
66
|
return { event: commands.findByClientTxn(room.id, command.input.actorId, command.input.clientTxnId), created: false, receipt };
|
|
67
|
+
if (command.durableCommand)
|
|
68
|
+
messageCommands.assertAdmissionUnblocked(command.session.id, command.durableCommand.delivery);
|
|
57
69
|
const preparedCommand = commandInput ? messageCommands.prepare(commandInput) : undefined;
|
|
58
70
|
const createdAt = command.input.createdAt ?? new Date().toISOString();
|
|
59
71
|
const preparedPayload = ingest.prepareUserMessagePayload(command.text, createdAt);
|
|
@@ -65,6 +77,8 @@ function execute(command) {
|
|
|
65
77
|
throw Object.assign(new Error("Transaction belongs to the legacy admission contract."), { code: "command_conflict" });
|
|
66
78
|
return { event: commands.findByClientTxn(room.id, command.input.actorId, command.input.clientTxnId), created: false, receipt };
|
|
67
79
|
}
|
|
80
|
+
if (command.durableCommand)
|
|
81
|
+
messageCommands.assertAdmissionUnblocked(command.session.id, command.durableCommand.delivery);
|
|
68
82
|
const event = commands.appendEvent({ ...command.input, createdAt });
|
|
69
83
|
sessions.upsertSession(command.session, command.durableCommand ? sessions.getSession(command.session.id)?.status ?? "idle" : "idle", command.session.updatedAt, { preserveRuntimeBinding: true });
|
|
70
84
|
ingest.ingestUserMessageAccepted({ session: command.session, roomId: room.id, actorId: command.input.actorId ?? "", text: command.text, clientTxnId: command.input.clientTxnId, eventId: command.durableCommand?.eventId, legacyEvent: event, preparedPayload });
|
|
@@ -85,10 +99,14 @@ function execute(command) {
|
|
|
85
99
|
case "ingestOutput": {
|
|
86
100
|
const result = ingest.ingestOutputEvent(command.input);
|
|
87
101
|
messageCommands.recordOutput(command.input.session.id, "eventId" in command.input.event ? command.input.event.eventId : undefined, command.input.event.type);
|
|
88
|
-
const row = store.db.prepare("SELECT created_at, event_id FROM event_log WHERE stream_id = ?").get(result.streamId);
|
|
102
|
+
const row = store.db.prepare("SELECT created_at, event_id, attributes_json FROM event_log WHERE stream_id = ?").get(result.streamId);
|
|
89
103
|
if (!row)
|
|
90
104
|
throw new Error(`Missing output event ${result.streamId} after ingest.`);
|
|
91
|
-
|
|
105
|
+
const attributes = JSON.parse(row.attributes_json);
|
|
106
|
+
const enrichment = command.input.event.type === "compaction_end" && attributes.compactionStats
|
|
107
|
+
? { compactionStats: attributes.compactionStats }
|
|
108
|
+
: undefined;
|
|
109
|
+
return { ...result, stored: { createdAt: row.created_at, eventId: row.event_id ?? String(result.streamId) }, enrichment };
|
|
92
110
|
}
|
|
93
111
|
case "status": return { operations, busyRetries, lastOperationMs, pid: process.pid, synchronous: store.db.prepare("PRAGMA synchronous").get(), journalMode: store.db.prepare("PRAGMA journal_mode").get() };
|
|
94
112
|
}
|
|
@@ -122,7 +140,9 @@ function attempt(request) {
|
|
|
122
140
|
}
|
|
123
141
|
const domainCode = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
124
142
|
if (domainCode === "room_not_found" || domainCode === "room_read_only" || (domainCode.startsWith("storage_") || domainCode.startsWith("command_")) || domainCode === "pibo_output_identity_collision") {
|
|
125
|
-
|
|
143
|
+
const source = error;
|
|
144
|
+
const details = Object.fromEntries(["retryable", "scope", "blockingCommandId", "blockedSince", "oldestWaitAgeMs", "nextAction"].flatMap(key => source[key] === undefined ? [] : [[key, source[key]]]));
|
|
145
|
+
respond(request, { error: { code: domainCode, message, ...(Object.keys(details).length ? { details } : {}) } });
|
|
126
146
|
return;
|
|
127
147
|
}
|
|
128
148
|
respond(request, { error: { code: /database is (?:locked|busy)/i.test(message) ? "storage_busy" : "storage_operation_failed", message: "Storage operation failed; reconcile the transaction ID before retrying." } });
|
|
@@ -142,5 +162,6 @@ port.postMessage({
|
|
|
142
162
|
...workerStatus(),
|
|
143
163
|
journalMode: store.db.prepare("PRAGMA journal_mode").get(),
|
|
144
164
|
synchronous: store.db.prepare("PRAGMA synchronous").get(),
|
|
165
|
+
startupReconciliation,
|
|
145
166
|
},
|
|
146
167
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { outputIdentityFingerprint, outputPartFingerprint } from "../core/output-render-sequence.js";
|
|
2
|
+
import { legacyOutputIdentityFingerprintCandidates, OUTPUT_IDENTITY_FINGERPRINT_VERSION, outputIdentityFieldDigests, outputIdentityFingerprint, outputPartFingerprint } from "../core/output-render-sequence.js";
|
|
3
3
|
import { rootSessionId } from "./session-store.js";
|
|
4
4
|
export class PiboOutputIdentityCollisionError extends Error {
|
|
5
5
|
idempotencyKey;
|
|
@@ -26,6 +26,7 @@ export function outputPersistenceErrorIsRetryable(error) {
|
|
|
26
26
|
}
|
|
27
27
|
const INLINE_MESSAGE_PAYLOAD_THRESHOLD_BYTES = 16 * 1024;
|
|
28
28
|
const INLINE_JSON_PAYLOAD_THRESHOLD_BYTES = 16 * 1024;
|
|
29
|
+
const MAX_FINGERPRINT_FIELD_DIFFERENCES = 24;
|
|
29
30
|
export class ChatDataIngestService {
|
|
30
31
|
store;
|
|
31
32
|
constructor(store) {
|
|
@@ -106,15 +107,30 @@ export class ChatDataIngestService {
|
|
|
106
107
|
const event = input.event;
|
|
107
108
|
const idempotencyKey = outputIdempotencyKey(event);
|
|
108
109
|
const identityFingerprint = outputIdentityFingerprint(event);
|
|
110
|
+
const identityFieldDigests = outputIdentityFieldDigests(event);
|
|
111
|
+
const incomingProvenance = input.persistenceProvenance ?? {
|
|
112
|
+
producer: "direct-ingest",
|
|
113
|
+
projection: "product-history",
|
|
114
|
+
phase: "direct",
|
|
115
|
+
};
|
|
109
116
|
const partFingerprint = isOutputPartEvent(event) ? outputPartFingerprint(event) : undefined;
|
|
110
117
|
if (idempotencyKey) {
|
|
111
|
-
const
|
|
118
|
+
const directExisting = this.store.eventLog.findByIdempotencyKey(idempotencyKey);
|
|
119
|
+
const legacyExisting = !directExisting && event.type === "execution_result"
|
|
120
|
+
? this.store.eventLog.findByIdempotencyKey(legacyOutputIdempotencyKey(event))
|
|
121
|
+
: undefined;
|
|
122
|
+
const legacyPhaseMatches = legacyExisting && event.type === "execution_result"
|
|
123
|
+
? legacyExecutionResultPhase(legacyExisting.attributes) === executionResultPhase(event)
|
|
124
|
+
: false;
|
|
125
|
+
const existing = directExisting ?? (legacyExisting && (storedFingerprintMatches(legacyExisting.attributes, event, identityFingerprint) || legacyPhaseMatches) ? legacyExisting : undefined);
|
|
112
126
|
if (existing) {
|
|
113
127
|
const existingFingerprint = typeof existing.attributes.identityFingerprint === "string"
|
|
114
128
|
? existing.attributes.identityFingerprint
|
|
115
129
|
: undefined;
|
|
116
|
-
if (existingFingerprint &&
|
|
130
|
+
if (existingFingerprint && !storedFingerprintMatches(existing.attributes, event, identityFingerprint)) {
|
|
117
131
|
const now = input.createdAt ?? new Date().toISOString();
|
|
132
|
+
const existingFieldDigests = stringMap(existing.attributes.identityFieldDigests);
|
|
133
|
+
const fieldDifferences = diffFieldDigests(existingFieldDigests, identityFieldDigests);
|
|
118
134
|
this.store.eventLog.appendEvent({
|
|
119
135
|
sessionId: input.session.id,
|
|
120
136
|
sessionSequence: this.nextEventSequence(input.session.id),
|
|
@@ -133,6 +149,10 @@ export class ChatDataIngestService {
|
|
|
133
149
|
existingFingerprint,
|
|
134
150
|
incomingFingerprint: identityFingerprint,
|
|
135
151
|
incomingType: event.type,
|
|
152
|
+
existingProvenance: redactedPersistenceProvenance(existing.attributes.persistenceProvenance),
|
|
153
|
+
incomingProvenance,
|
|
154
|
+
fieldDifferences,
|
|
155
|
+
fieldDifferencesTruncated: fieldDifferences.length >= MAX_FINGERPRINT_FIELD_DIFFERENCES,
|
|
136
156
|
},
|
|
137
157
|
createdAt: now,
|
|
138
158
|
indexedAt: now,
|
|
@@ -177,6 +197,9 @@ export class ChatDataIngestService {
|
|
|
177
197
|
previewText: previewTextForOutputEvent(event),
|
|
178
198
|
attributes: compactObject({
|
|
179
199
|
identityFingerprint,
|
|
200
|
+
identityFingerprintVersion: OUTPUT_IDENTITY_FINGERPRINT_VERSION,
|
|
201
|
+
identityFieldDigests,
|
|
202
|
+
persistenceProvenance: incomingProvenance,
|
|
180
203
|
outputPartFingerprint: partFingerprint,
|
|
181
204
|
eventIdentityScoped: eventIdForOutputEvent(event) !== undefined,
|
|
182
205
|
semanticEventId: eventIdForOutputEvent(event),
|
|
@@ -340,6 +363,43 @@ function nonNegativeFiniteNumber(value) {
|
|
|
340
363
|
function isRecord(value) {
|
|
341
364
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
342
365
|
}
|
|
366
|
+
function stringMap(value) {
|
|
367
|
+
if (!isRecord(value))
|
|
368
|
+
return {};
|
|
369
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string").slice(0, 64));
|
|
370
|
+
}
|
|
371
|
+
function diffFieldDigests(existing, incoming) {
|
|
372
|
+
return [...new Set([...Object.keys(existing), ...Object.keys(incoming)])]
|
|
373
|
+
.sort()
|
|
374
|
+
.filter((field) => existing[field] !== incoming[field])
|
|
375
|
+
.slice(0, MAX_FINGERPRINT_FIELD_DIFFERENCES)
|
|
376
|
+
.map((field) => ({
|
|
377
|
+
field,
|
|
378
|
+
change: existing[field] === undefined ? "added" : incoming[field] === undefined ? "removed" : "changed",
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
function redactedPersistenceProvenance(value) {
|
|
382
|
+
if (!isRecord(value))
|
|
383
|
+
return { producer: "unknown", projection: "product-history", phase: "unknown" };
|
|
384
|
+
const bounded = (field) => typeof value[field] === "string" ? value[field].slice(0, 64) : "unknown";
|
|
385
|
+
return { producer: bounded("producer"), projection: bounded("projection"), phase: bounded("phase") };
|
|
386
|
+
}
|
|
387
|
+
function executionResultPhase(event) {
|
|
388
|
+
return isRecord(event.result) && event.result.queued === true ? "queued" : "complete";
|
|
389
|
+
}
|
|
390
|
+
function legacyExecutionResultPhase(attributes) {
|
|
391
|
+
return isRecord(attributes.inlinePayload) && attributes.inlinePayload.queued === true ? "queued" : "complete";
|
|
392
|
+
}
|
|
393
|
+
function storedFingerprintMatches(attributes, event, currentFingerprint) {
|
|
394
|
+
const fingerprint = attributes.identityFingerprint;
|
|
395
|
+
if (typeof fingerprint !== "string")
|
|
396
|
+
return true;
|
|
397
|
+
if (attributes.identityFingerprintVersion === OUTPUT_IDENTITY_FINGERPRINT_VERSION)
|
|
398
|
+
return fingerprint === currentFingerprint;
|
|
399
|
+
// Versionless fingerprints were produced by v1. Compare with the exact old
|
|
400
|
+
// algorithm instead of comparing incompatible hash formats.
|
|
401
|
+
return legacyOutputIdentityFingerprintCandidates(event).includes(fingerprint);
|
|
402
|
+
}
|
|
343
403
|
function deterministicId(prefix, value) {
|
|
344
404
|
return `${prefix}_${createHash("sha256").update(value).digest("hex").slice(0, 32)}`;
|
|
345
405
|
}
|
|
@@ -378,6 +438,13 @@ export function outputPersistenceDeliveryKey(event) {
|
|
|
378
438
|
return outputIdempotencyKey(event)
|
|
379
439
|
?? `pibo.output:${event.piboSessionId}:${event.type}:render:${event.renderSequence ?? "unpositioned"}`;
|
|
380
440
|
}
|
|
441
|
+
/** Accepted only while recovering pre-phase execution-result envelopes. */
|
|
442
|
+
export function legacyOutputIdempotencyKey(event) {
|
|
443
|
+
if (event.type !== "execution_result")
|
|
444
|
+
return outputIdempotencyKey(event);
|
|
445
|
+
const base = event.eventId;
|
|
446
|
+
return base ? `pibo.output:${event.piboSessionId}:${event.type}:${base}:${event.action}` : undefined;
|
|
447
|
+
}
|
|
381
448
|
function outputPartKey(event) {
|
|
382
449
|
if (event.type === "tool_call")
|
|
383
450
|
return `${event.toolCallId}:${event.toolInvocationOrdinal ?? 0}:${event.argsComplete ? "complete" : "partial"}:${hashJson(event.args)}`;
|
|
@@ -394,7 +461,7 @@ function outputPartKey(event) {
|
|
|
394
461
|
if (event.type === "compaction_start" || event.type === "compaction_end")
|
|
395
462
|
return String(event.compactionIndex ?? 0);
|
|
396
463
|
if (event.type === "execution_result")
|
|
397
|
-
return event.action
|
|
464
|
+
return `${event.action}:${executionResultPhase(event)}`;
|
|
398
465
|
return "main";
|
|
399
466
|
}
|
|
400
467
|
function messageIdForOutputEvent(event) {
|
|
@@ -479,6 +546,8 @@ function specificAttributesForOutputEvent(event) {
|
|
|
479
546
|
if (event.type === "assistant_usage")
|
|
480
547
|
return {
|
|
481
548
|
usageIndex: event.usageIndex,
|
|
549
|
+
inferenceId: event.inferenceId,
|
|
550
|
+
inferenceTarget: event.inferenceTarget,
|
|
482
551
|
inputTokens: event.inputTokens,
|
|
483
552
|
outputTokens: event.outputTokens,
|
|
484
553
|
cacheReadTokens: event.cacheReadTokens,
|