@pasko70/pibo 1.12.0 → 1.12.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/apps/chat/chat-trace-helpers.js +59 -2
- package/dist/apps/chat/data/timeline-query-service.js +1 -1
- package/dist/apps/chat/web-app.js +16 -8
- package/dist/apps/chat-ui/assets/{dist-DiPvo4rq.js → dist-3bD-92Wo.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DxJWicji.js → dist-BFjy5Y59.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D1U9ck8z.js → dist-BKwQlbhS.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-B2Pozq2c.js → dist-Cp68y_h5.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DYdgxFZX.js → dist-DE0y_EI7.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Ci4lft6y.js → dist-DPb0yDi4.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CkLit_da.js → dist-D_dQ5GaA.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Bgv5Fm3v.js → dist-Daq2KRpq.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D4qbF8wy.js → dist-aZnuCezL.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-la7ZfDGk.js → dist-q6Vpm5yH.js} +1 -1
- package/dist/apps/chat-ui/assets/index-DXPKuGfs.js +237 -0
- package/dist/apps/chat-ui/index.html +1 -1
- package/dist/apps/chat-vscode-web/assets/index-B3dSrp-L.js +41 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.12.1.vsix +0 -0
- package/dist/core/gateway-resource-guard.js +46 -15
- package/dist/core/routed-session.js +40 -1
- package/dist/core/runtime.js +2 -0
- package/dist/core/session-errors.js +3 -0
- package/dist/core/session-router.js +25 -2
- package/dist/core/transcript-integrity.js +431 -0
- package/dist/data/pibo-store.js +2 -0
- package/dist/data/telemetry.js +148 -0
- package/dist/debug/index.js +11 -0
- package/dist/gateway/server.js +1 -0
- package/dist/reliability/store.js +11 -6
- package/dist/runs/registry.js +38 -1
- package/dist/runs/resource-isolation.js +437 -0
- package/dist/runs/tools.js +6 -3
- package/dist/sessions/pibo-data-store.js +114 -0
- package/dist/shared/trace-engine.js +3 -3
- package/dist/shared/trace-event-projection.js +165 -36
- package/dist/shared/trace-nodes.js +6 -4
- package/dist/shared/trace-page-merge.js +106 -2
- package/dist/shared/trace-transcript.js +194 -36
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-CYxPvrxL.js +0 -237
- package/dist/apps/chat-vscode-web/assets/index-CFSHKXsQ.js +0 -41
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.12.0.vsix +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ChatDataIngestService } from "../data/ingest-service.js";
|
|
1
2
|
import { PiboDataStore } from "../data/pibo-store.js";
|
|
2
3
|
import { createPiboSession, matchesFindInput, } from "./store.js";
|
|
3
4
|
export class PiboDataSessionStore {
|
|
@@ -119,6 +120,70 @@ export class PiboDataSessionStore {
|
|
|
119
120
|
getTelemetryStore() {
|
|
120
121
|
return this.dataStore.telemetry;
|
|
121
122
|
}
|
|
123
|
+
recoverInterruptedRuntimeState(input = {}) {
|
|
124
|
+
const at = input.at ?? new Date().toISOString();
|
|
125
|
+
const runsBySession = groupRunsByController(input.recoveredRuns ?? []);
|
|
126
|
+
return this.dataStore.transaction(() => {
|
|
127
|
+
const recoveredTurns = this.dataStore.telemetry.recoverInterruptedTurns({
|
|
128
|
+
at,
|
|
129
|
+
resolveOutcome: (turn) => recoveryOutcomeForTurn(turn, runsBySession.get(turn.piboSessionId) ?? []),
|
|
130
|
+
});
|
|
131
|
+
if (recoveredTurns.length === 0)
|
|
132
|
+
return [];
|
|
133
|
+
const ingest = new ChatDataIngestService(this.dataStore);
|
|
134
|
+
const results = [];
|
|
135
|
+
for (const recovered of recoveredTurns) {
|
|
136
|
+
const session = this.get(recovered.turn.piboSessionId);
|
|
137
|
+
if (!session)
|
|
138
|
+
continue;
|
|
139
|
+
const row = this.db.prepare("SELECT room_id FROM sessions WHERE id = ?").get(session.id);
|
|
140
|
+
const event = {
|
|
141
|
+
type: "session_error",
|
|
142
|
+
piboSessionId: session.id,
|
|
143
|
+
eventId: recoveryEventId(recovered.turn),
|
|
144
|
+
error: recovered.outcome.summary,
|
|
145
|
+
errorDetails: {
|
|
146
|
+
category: "runtime_restart",
|
|
147
|
+
errorClass: recovered.outcome.status === "aborted" ? "runtime_abort" : "runtime_error",
|
|
148
|
+
code: recovered.outcome.status === "timeout" ? "timeout" : "runtime_interrupted",
|
|
149
|
+
origin: "runtime",
|
|
150
|
+
severity: "error",
|
|
151
|
+
retryable: false,
|
|
152
|
+
userMessage: "The previous gateway runtime ended before this turn completed.",
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
this.db.prepare(`
|
|
156
|
+
UPDATE sessions SET
|
|
157
|
+
status = 'error',
|
|
158
|
+
updated_at = ?,
|
|
159
|
+
last_activity_at = MAX(last_activity_at, ?)
|
|
160
|
+
WHERE id = ? AND deleted_at IS NULL
|
|
161
|
+
`).run(at, at, session.id);
|
|
162
|
+
this.db.prepare(`
|
|
163
|
+
UPDATE session_navigation SET
|
|
164
|
+
status = 'error',
|
|
165
|
+
last_activity_at = MAX(last_activity_at, ?),
|
|
166
|
+
sort_key = MAX(sort_key, ?),
|
|
167
|
+
updated_at = ?
|
|
168
|
+
WHERE session_id = ?
|
|
169
|
+
`).run(at, at, at, session.id);
|
|
170
|
+
ingest.ingestOutputEvent({
|
|
171
|
+
session,
|
|
172
|
+
roomId: row?.room_id ?? undefined,
|
|
173
|
+
actorId: session.id,
|
|
174
|
+
event,
|
|
175
|
+
createdAt: at,
|
|
176
|
+
});
|
|
177
|
+
results.push({
|
|
178
|
+
turnId: recovered.turn.turnId,
|
|
179
|
+
piboSessionId: session.id,
|
|
180
|
+
event,
|
|
181
|
+
outcome: recovered.outcome,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return results;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
122
187
|
close() {
|
|
123
188
|
if (this.ownsDataStore)
|
|
124
189
|
this.dataStore.close();
|
|
@@ -137,6 +202,55 @@ export class PiboDataSessionStore {
|
|
|
137
202
|
export function createDefaultPiboDataSessionStore() {
|
|
138
203
|
return new PiboDataSessionStore(new PiboDataStore());
|
|
139
204
|
}
|
|
205
|
+
function groupRunsByController(runs) {
|
|
206
|
+
const grouped = new Map();
|
|
207
|
+
for (const run of runs) {
|
|
208
|
+
const items = grouped.get(run.controllerPiboSessionId) ?? [];
|
|
209
|
+
items.push(run);
|
|
210
|
+
grouped.set(run.controllerPiboSessionId, items);
|
|
211
|
+
}
|
|
212
|
+
return grouped;
|
|
213
|
+
}
|
|
214
|
+
function recoveryOutcomeForTurn(turn, runs) {
|
|
215
|
+
if (turn.status === "queued") {
|
|
216
|
+
return {
|
|
217
|
+
status: "aborted",
|
|
218
|
+
summary: "Queued turn was interrupted by gateway restart before it could start.",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const timedOut = runs.find((run) => run.status === "timed_out");
|
|
222
|
+
if (timedOut) {
|
|
223
|
+
return {
|
|
224
|
+
status: "timeout",
|
|
225
|
+
summary: `Gateway restart recovery timed out yielded run ${timedOut.runId} before this turn completed.`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const failed = runs.find((run) => run.status === "failed");
|
|
229
|
+
if (failed) {
|
|
230
|
+
return {
|
|
231
|
+
status: "error",
|
|
232
|
+
summary: `Gateway restart recovery failed yielded run ${failed.runId} before this turn completed.`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const queued = runs.find((run) => run.status === "queued");
|
|
236
|
+
if (queued) {
|
|
237
|
+
return {
|
|
238
|
+
status: "aborted",
|
|
239
|
+
summary: `Gateway restart interrupted this turn; yielded run ${queued.runId} was queued for retry.`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
status: "aborted",
|
|
244
|
+
summary: "Turn was interrupted by gateway restart.",
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function recoveryEventId(turn) {
|
|
248
|
+
if (turn.eventId)
|
|
249
|
+
return turn.eventId;
|
|
250
|
+
if (turn.inputEventId)
|
|
251
|
+
return turn.inputEventId;
|
|
252
|
+
return turn.turnId.startsWith("turn_") ? turn.turnId.slice("turn_".length) : turn.turnId;
|
|
253
|
+
}
|
|
140
254
|
function sessionFromRow(row) {
|
|
141
255
|
return {
|
|
142
256
|
id: row.id,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { reconcileAsyncAgentRunStatuses } from "./trace-async-agent-runs.js";
|
|
2
|
-
import { applySingleEventToNodes, contentDeltaPatchNodeId, dedupeTraceEvents, eventsCanAffectAsyncAgentRunStatus, findOpenTranscriptEventIds, isConfirmedUserMessageEcho, latestTraceStreamId, mergeMessageTurnTimings, messageTurnTimingsFromEvents,
|
|
2
|
+
import { applySingleEventToNodes, contentDeltaPatchNodeId, dedupeTraceEvents, eventsCanAffectAsyncAgentRunStatus, findOpenTranscriptEventIds, isConfirmedUserMessageEcho, latestTraceStreamId, mergeMessageTurnTimings, messageTurnTimingsFromEvents, reconcileTranscriptUserMessages, traceEventDedupeKey, } from "./trace-event-projection.js";
|
|
3
3
|
import { flattenTraceNodes, mapTraceNodesById, nestTraceNodes } from "./trace-nodes.js";
|
|
4
4
|
import { nestMutableCopiedTraceNodes, shareUnchangedTraceNodes } from "./trace-patch-nodes.js";
|
|
5
5
|
import { mapTraceChildSessionsByParent, mapTraceSubagentSessionLinks, } from "./trace-subagent-links.js";
|
|
@@ -13,10 +13,10 @@ export function buildTraceViewFromEvents(input) {
|
|
|
13
13
|
const events = dedupeTraceEvents(input.events);
|
|
14
14
|
const allEntries = input.transcriptEntries ?? [];
|
|
15
15
|
const openTranscriptEventIds = findOpenTranscriptEventIds(events, sessionStatus);
|
|
16
|
-
const entries = projectTranscriptEntries(allEntries, sessionStatus, openTranscriptEventIds);
|
|
17
16
|
const turnTimings = mergeMessageTurnTimings(input.turnTimings ?? [], messageTurnTimingsFromEvents(events));
|
|
17
|
+
const entries = projectTranscriptEntries(allEntries, sessionStatus, openTranscriptEventIds, turnTimings);
|
|
18
18
|
const nodes = traceNodesFromEntries(input.session.id, entries, turnTimings);
|
|
19
|
-
|
|
19
|
+
reconcileTranscriptUserMessages(nodes, events, turnTimings);
|
|
20
20
|
const byId = mapTraceNodesById(nodes);
|
|
21
21
|
const childByParent = mapTraceChildSessionsByParent(input.sessions ?? []);
|
|
22
22
|
const linkedChildByToolCallId = mapTraceSubagentSessionLinks(events);
|
|
@@ -30,7 +30,7 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
|
|
|
30
30
|
}
|
|
31
31
|
if (payload.type === "assistant_message") {
|
|
32
32
|
const node = assistantMessageNodeFromEvent(piboSessionId, payload, storedEvent.createdAt, storedEvent.eventSequence, storedEvent.streamId, storedEvent.streamFrameIndex);
|
|
33
|
-
const existing = byId.get(node.id);
|
|
33
|
+
const existing = byId.get(node.id) ?? findMatchingContentNode(byId, node);
|
|
34
34
|
if (existing) {
|
|
35
35
|
mergeAssistantMessageEvent(existing, node);
|
|
36
36
|
return;
|
|
@@ -59,7 +59,7 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
if (node.type === "assistant.message") {
|
|
62
|
-
const existing = byId.get(node.id);
|
|
62
|
+
const existing = byId.get(node.id) ?? findMatchingContentNode(byId, node);
|
|
63
63
|
if (existing) {
|
|
64
64
|
mergeAssistantMessageEvent(existing, node);
|
|
65
65
|
return;
|
|
@@ -73,7 +73,7 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
if (node.type === "model.reasoning") {
|
|
76
|
-
const existing = byId.get(node.id);
|
|
76
|
+
const existing = byId.get(node.id) ?? findMatchingContentNode(byId, node);
|
|
77
77
|
if (existing) {
|
|
78
78
|
mergeReasoningEvent(existing, node);
|
|
79
79
|
return;
|
|
@@ -194,26 +194,77 @@ export function contentDeltaPatchNodeId(event) {
|
|
|
194
194
|
}
|
|
195
195
|
return undefined;
|
|
196
196
|
}
|
|
197
|
-
export function
|
|
197
|
+
export function reconcileTranscriptUserMessages(nodes, events, turnTimings = []) {
|
|
198
198
|
const transcriptUsers = nodes.filter((node) => node.type === "user.message" && node.source === "transcript");
|
|
199
|
+
const timingByEventId = new Map(turnTimings.map((timing) => [timing.eventId, timing]));
|
|
200
|
+
let latestTranscriptUser;
|
|
201
|
+
for (const node of nodes) {
|
|
202
|
+
if (node.type === "user.message" && node.source === "transcript") {
|
|
203
|
+
latestTranscriptUser = node;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (node.type !== "assistant.message" ||
|
|
207
|
+
node.source !== "transcript" ||
|
|
208
|
+
!node.eventId ||
|
|
209
|
+
!latestTranscriptUser ||
|
|
210
|
+
transcriptUserMessageHasCanonicalIdentity(latestTranscriptUser))
|
|
211
|
+
continue;
|
|
212
|
+
const timing = timingByEventId.get(node.eventId);
|
|
213
|
+
if (timing?.userText &&
|
|
214
|
+
normalizedUserMessageText(traceNodeText(latestTranscriptUser)) !== normalizedUserMessageText(timing.userText))
|
|
215
|
+
continue;
|
|
216
|
+
assignTranscriptUserMessageIdentity(latestTranscriptUser, timing);
|
|
217
|
+
}
|
|
218
|
+
let timingCursor = transcriptUsers.length - 1;
|
|
219
|
+
for (let timingIndex = turnTimings.length - 1; timingIndex >= 0; timingIndex -= 1) {
|
|
220
|
+
const timing = turnTimings[timingIndex];
|
|
221
|
+
const prompt = normalizedUserMessageText(timing.userText);
|
|
222
|
+
if (!prompt)
|
|
223
|
+
continue;
|
|
224
|
+
const exactMatchIndex = transcriptUsers.findIndex((node, index) => index <= timingCursor &&
|
|
225
|
+
!transcriptUserMessageHasCanonicalIdentity(node) &&
|
|
226
|
+
node.entryId === timing.eventId);
|
|
227
|
+
let matchIndex = exactMatchIndex;
|
|
228
|
+
if (matchIndex === -1 && (timing.userMessageType === "message_steered" || timing.completedAt)) {
|
|
229
|
+
for (let userIndex = timingCursor; userIndex >= 0; userIndex -= 1) {
|
|
230
|
+
const node = transcriptUsers[userIndex];
|
|
231
|
+
if (transcriptUserMessageHasCanonicalIdentity(node))
|
|
232
|
+
continue;
|
|
233
|
+
if (normalizedUserMessageText(traceNodeText(node)) !== prompt)
|
|
234
|
+
continue;
|
|
235
|
+
matchIndex = userIndex;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (matchIndex === -1)
|
|
240
|
+
continue;
|
|
241
|
+
const matchedNode = transcriptUsers[matchIndex];
|
|
242
|
+
assignTranscriptUserMessageIdentity(matchedNode, timing);
|
|
243
|
+
timingCursor = matchIndex - 1;
|
|
244
|
+
}
|
|
199
245
|
let userCursor = 0;
|
|
200
246
|
for (const storedEvent of events) {
|
|
201
247
|
const event = storedEvent.payload;
|
|
202
248
|
if ((event.type !== "message_queued" && event.type !== "message_steered") || event.source !== "user")
|
|
203
249
|
continue;
|
|
204
|
-
const
|
|
250
|
+
const payloadEventId = typeof event.eventId === "string" ? event.eventId : undefined;
|
|
251
|
+
const eventId = payloadEventId ?? storedEvent.eventId;
|
|
252
|
+
const canonicalId = `event:${event.type}:${payloadEventId ?? cryptoSafeId(event)}`;
|
|
253
|
+
const stableKey = eventStableKey(event);
|
|
205
254
|
const text = typeof event.text === "string" ? event.text : undefined;
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
});
|
|
255
|
+
const identityMatchIndex = transcriptUsers.findIndex((node) => transcriptUserMessageMatchesIdentity(node, canonicalId, stableKey, eventId));
|
|
256
|
+
const matchIndex = identityMatchIndex !== -1
|
|
257
|
+
? identityMatchIndex
|
|
258
|
+
: transcriptUsers.findIndex((node, index) => index >= userCursor &&
|
|
259
|
+
!transcriptUserMessageHasCanonicalIdentity(node) &&
|
|
260
|
+
Boolean(text && traceNodeText(node) === text));
|
|
213
261
|
if (matchIndex === -1)
|
|
214
262
|
continue;
|
|
215
|
-
transcriptUsers[matchIndex]
|
|
216
|
-
|
|
263
|
+
const matchedNode = transcriptUsers[matchIndex];
|
|
264
|
+
matchedNode.id = canonicalId;
|
|
265
|
+
matchedNode.stableKey = stableKey;
|
|
266
|
+
matchedNode.startedAt = storedEvent.createdAt;
|
|
267
|
+
userCursor = Math.max(userCursor, matchIndex + 1);
|
|
217
268
|
}
|
|
218
269
|
}
|
|
219
270
|
export function isConfirmedUserMessageEcho(nodes, event) {
|
|
@@ -223,15 +274,46 @@ function confirmedUserMessageEchoNode(nodes, event) {
|
|
|
223
274
|
const payload = event.payload;
|
|
224
275
|
if ((payload.type !== "message_queued" && payload.type !== "message_steered") || payload.source !== "user")
|
|
225
276
|
return undefined;
|
|
226
|
-
const
|
|
277
|
+
const payloadEventId = typeof payload.eventId === "string" ? payload.eventId : undefined;
|
|
278
|
+
const eventId = payloadEventId ?? event.eventId;
|
|
279
|
+
const canonicalId = `event:${payload.type}:${payloadEventId ?? cryptoSafeId(payload)}`;
|
|
280
|
+
const stableKey = eventStableKey(payload);
|
|
227
281
|
const text = typeof payload.text === "string" ? payload.text : undefined;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
282
|
+
const transcriptUsers = flattenTraceNodes([...nodes]).filter((node) => node.type === "user.message" && node.source === "transcript");
|
|
283
|
+
const identityMatch = transcriptUsers.find((node) => transcriptUserMessageMatchesIdentity(node, canonicalId, stableKey, eventId));
|
|
284
|
+
if (identityMatch)
|
|
285
|
+
return identityMatch;
|
|
286
|
+
return transcriptUsers.find((node) => !transcriptUserMessageHasCanonicalIdentity(node) && Boolean(text && traceNodeText(node) === text));
|
|
287
|
+
}
|
|
288
|
+
function transcriptUserMessageMatchesIdentity(node, canonicalId, stableKey, eventId) {
|
|
289
|
+
if (node.id === canonicalId || node.stableKey === stableKey)
|
|
290
|
+
return true;
|
|
291
|
+
if (!eventId)
|
|
292
|
+
return false;
|
|
293
|
+
if (node.entryId === eventId || node.stableKey === `entry:${eventId}`)
|
|
294
|
+
return true;
|
|
295
|
+
return [node.id, node.stableKey].some((value) => canonicalUserMessageEventId(value) === eventId);
|
|
296
|
+
}
|
|
297
|
+
function transcriptUserMessageHasCanonicalIdentity(node) {
|
|
298
|
+
return [node.id, node.stableKey].some((value) => canonicalUserMessageEventId(value) !== undefined);
|
|
299
|
+
}
|
|
300
|
+
function canonicalUserMessageEventId(value) {
|
|
301
|
+
for (const prefix of ["event:message_queued:", "event:message_steered:"]) {
|
|
302
|
+
if (value?.startsWith(prefix))
|
|
303
|
+
return value.slice(prefix.length);
|
|
304
|
+
}
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
function assignTranscriptUserMessageIdentity(node, timing) {
|
|
308
|
+
if (!timing)
|
|
309
|
+
return;
|
|
310
|
+
const userMessageType = timing.userMessageType ?? "message_queued";
|
|
311
|
+
node.id = `event:${userMessageType}:${timing.eventId}`;
|
|
312
|
+
node.stableKey = `event:${userMessageType}:${timing.eventId}`;
|
|
313
|
+
}
|
|
314
|
+
function normalizedUserMessageText(value) {
|
|
315
|
+
const normalized = value?.replace(/\s+/g, " ").trim();
|
|
316
|
+
return normalized || undefined;
|
|
235
317
|
}
|
|
236
318
|
function traceNodeText(node) {
|
|
237
319
|
if (typeof node.output === "string")
|
|
@@ -505,6 +587,11 @@ function mergeAssistantDeltaEvent(nodes, byId, event, sessionStatus, createdAt,
|
|
|
505
587
|
nodes.push(node);
|
|
506
588
|
byId.set(node.id, node);
|
|
507
589
|
}
|
|
590
|
+
function findMatchingContentNode(byId, update) {
|
|
591
|
+
if (!update.stableKey)
|
|
592
|
+
return undefined;
|
|
593
|
+
return [...byId.values()].find((candidate) => candidate.type === update.type && candidate.stableKey === update.stableKey);
|
|
594
|
+
}
|
|
508
595
|
function mergeAssistantMessageEvent(target, update) {
|
|
509
596
|
target.status = update.status;
|
|
510
597
|
target.summary = update.summary ?? target.summary;
|
|
@@ -517,7 +604,8 @@ function mergeThinkingDeltaEvent(nodes, byId, event, sessionStatus, createdAt, e
|
|
|
517
604
|
return;
|
|
518
605
|
const thinkingId = thinkingEventNodeId(event);
|
|
519
606
|
const id = thinkingId ? thinkingNodeId(thinkingId) : `event:thinking_delta:${cryptoSafeId(event)}`;
|
|
520
|
-
const
|
|
607
|
+
const stableKey = thinkingId ? `reasoning:${thinkingId}` : id;
|
|
608
|
+
const existing = byId.get(id) ?? [...byId.values()].find((candidate) => candidate.type === "model.reasoning" && candidate.stableKey === stableKey);
|
|
521
609
|
if (existing) {
|
|
522
610
|
const text = `${typeof existing.output === "string" ? existing.output : ""}${event.text}`;
|
|
523
611
|
existing.status = sessionStatus === "running" ? "running" : "done";
|
|
@@ -537,7 +625,7 @@ function mergeThinkingDeltaEvent(nodes, byId, event, sessionStatus, createdAt, e
|
|
|
537
625
|
summary: event.text,
|
|
538
626
|
output: event.text,
|
|
539
627
|
source: "event-log",
|
|
540
|
-
stableKey
|
|
628
|
+
stableKey,
|
|
541
629
|
orderKey: eventTraceNodeOrder(eventSequence, event.type, streamId, streamFrameIndex),
|
|
542
630
|
children: [],
|
|
543
631
|
};
|
|
@@ -583,12 +671,17 @@ export function mergeMessageTurnTimings(...groups) {
|
|
|
583
671
|
byEventId.set(timing.eventId, timing);
|
|
584
672
|
continue;
|
|
585
673
|
}
|
|
674
|
+
const reasoningIndices = mergeUniqueIndices(existing.reasoningIndices, timing.reasoningIndices);
|
|
675
|
+
const assistantIndices = mergeUniqueIndices(existing.assistantIndices, timing.assistantIndices);
|
|
586
676
|
const merged = {
|
|
587
677
|
eventId: timing.eventId,
|
|
588
678
|
userText: timing.userText ?? existing.userText,
|
|
679
|
+
userMessageType: timing.userMessageType ?? existing.userMessageType,
|
|
589
680
|
startedAt: timing.startedAt ?? existing.startedAt,
|
|
590
681
|
completedAt: timing.completedAt ?? existing.completedAt,
|
|
591
682
|
durationMs: timing.durationMs ?? existing.durationMs,
|
|
683
|
+
...(reasoningIndices ? { reasoningIndices } : {}),
|
|
684
|
+
...(assistantIndices ? { assistantIndices } : {}),
|
|
592
685
|
};
|
|
593
686
|
if (merged.durationMs === undefined) {
|
|
594
687
|
const startedAtMs = parseTimestamp(merged.startedAt);
|
|
@@ -601,41 +694,74 @@ export function mergeMessageTurnTimings(...groups) {
|
|
|
601
694
|
}
|
|
602
695
|
return [...byEventId.values()];
|
|
603
696
|
}
|
|
697
|
+
function mergeUniqueIndices(first, second) {
|
|
698
|
+
if (!first?.length && !second?.length)
|
|
699
|
+
return undefined;
|
|
700
|
+
return [...new Set([...(first ?? []), ...(second ?? [])])];
|
|
701
|
+
}
|
|
702
|
+
function appendUniqueIndex(indices, partIndex) {
|
|
703
|
+
if (indices?.includes(partIndex))
|
|
704
|
+
return indices;
|
|
705
|
+
return [...(indices ?? []), partIndex];
|
|
706
|
+
}
|
|
604
707
|
export function messageTurnTimingsFromEvents(events) {
|
|
605
708
|
const timings = new Map();
|
|
606
|
-
const
|
|
607
|
-
const
|
|
709
|
+
const eventIds = [];
|
|
710
|
+
const seenEventIds = new Set();
|
|
608
711
|
const ignoredEventIds = new Set();
|
|
609
712
|
for (const storedEvent of events) {
|
|
610
713
|
const event = storedEvent.payload;
|
|
611
|
-
if (event.type !== "
|
|
714
|
+
if (event.type !== "message_queued" &&
|
|
715
|
+
event.type !== "message_steered" &&
|
|
716
|
+
event.type !== "message_started" &&
|
|
717
|
+
event.type !== "message_finished" &&
|
|
718
|
+
event.type !== "session_error" &&
|
|
719
|
+
event.type !== "thinking_finished" &&
|
|
720
|
+
event.type !== "assistant_message")
|
|
612
721
|
continue;
|
|
613
722
|
const eventId = typeof event.eventId === "string" ? event.eventId : undefined;
|
|
614
723
|
if (!eventId)
|
|
615
724
|
continue;
|
|
616
|
-
if (event.type === "message_started" && event.source === "service") {
|
|
725
|
+
if ((event.type === "message_queued" || event.type === "message_started") && event.source === "service") {
|
|
617
726
|
ignoredEventIds.add(eventId);
|
|
618
727
|
continue;
|
|
619
728
|
}
|
|
620
729
|
if (ignoredEventIds.has(eventId))
|
|
621
730
|
continue;
|
|
731
|
+
if (!seenEventIds.has(eventId)) {
|
|
732
|
+
eventIds.push(eventId);
|
|
733
|
+
seenEventIds.add(eventId);
|
|
734
|
+
}
|
|
622
735
|
const timing = timings.get(eventId) ?? {};
|
|
623
|
-
if (event.type === "
|
|
736
|
+
if (event.type === "message_queued") {
|
|
737
|
+
timing.userText ??= event.text;
|
|
738
|
+
}
|
|
739
|
+
else if (event.type === "message_steered") {
|
|
740
|
+
timing.userText ??= event.text;
|
|
741
|
+
timing.userMessageType = "message_steered";
|
|
742
|
+
}
|
|
743
|
+
else if (event.type === "message_started") {
|
|
624
744
|
timing.userText ??= event.text;
|
|
625
745
|
timing.startedAt ??= storedEvent.createdAt;
|
|
626
746
|
}
|
|
627
|
-
else {
|
|
747
|
+
else if (event.type === "message_finished" || event.type === "session_error") {
|
|
628
748
|
timing.completedAt = storedEvent.createdAt;
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
749
|
+
}
|
|
750
|
+
else if (event.type === "thinking_finished" && hasVisibleText(event.text)) {
|
|
751
|
+
const partIndex = typeof event.thinkingIndex === "number" ? event.thinkingIndex : event.contentIndex;
|
|
752
|
+
if (typeof partIndex === "number")
|
|
753
|
+
timing.reasoningIndices = appendUniqueIndex(timing.reasoningIndices, partIndex);
|
|
754
|
+
}
|
|
755
|
+
else if (event.type === "assistant_message" && hasVisibleText(event.text)) {
|
|
756
|
+
const partIndex = typeof event.assistantIndex === "number" ? event.assistantIndex : event.contentIndex;
|
|
757
|
+
if (typeof partIndex === "number")
|
|
758
|
+
timing.assistantIndices = appendUniqueIndex(timing.assistantIndices, partIndex);
|
|
633
759
|
}
|
|
634
760
|
timings.set(eventId, timing);
|
|
635
761
|
}
|
|
636
|
-
return
|
|
762
|
+
return eventIds.flatMap((eventId) => {
|
|
637
763
|
const timing = timings.get(eventId);
|
|
638
|
-
if (!timing
|
|
764
|
+
if (!timing)
|
|
639
765
|
return [];
|
|
640
766
|
const startedAtMs = parseTimestamp(timing.startedAt);
|
|
641
767
|
const completedAtMs = parseTimestamp(timing.completedAt);
|
|
@@ -643,10 +769,13 @@ export function messageTurnTimingsFromEvents(events) {
|
|
|
643
769
|
eventId,
|
|
644
770
|
userText: timing.userText,
|
|
645
771
|
startedAt: timing.startedAt,
|
|
772
|
+
...(timing.userMessageType ? { userMessageType: timing.userMessageType } : {}),
|
|
646
773
|
completedAt: timing.completedAt,
|
|
647
774
|
durationMs: startedAtMs === undefined || completedAtMs === undefined
|
|
648
775
|
? undefined
|
|
649
776
|
: Math.max(0, completedAtMs - startedAtMs),
|
|
777
|
+
...(timing.reasoningIndices ? { reasoningIndices: timing.reasoningIndices } : {}),
|
|
778
|
+
...(timing.assistantIndices ? { assistantIndices: timing.assistantIndices } : {}),
|
|
650
779
|
}];
|
|
651
780
|
});
|
|
652
781
|
}
|
|
@@ -19,14 +19,16 @@ function areTraceNodesSorted(nodes) {
|
|
|
19
19
|
return true;
|
|
20
20
|
}
|
|
21
21
|
export function compareTraceNodes(left, right) {
|
|
22
|
-
|
|
23
|
-
if (bySameTurnPhase !== 0)
|
|
24
|
-
return bySameTurnPhase;
|
|
25
|
-
if (left.orderKey?.sourceRank === right.orderKey?.sourceRank) {
|
|
22
|
+
if (left.orderKey && right.orderKey && left.orderKey.sourceRank === right.orderKey.sourceRank) {
|
|
26
23
|
const bySameSourceOrder = compareTraceOrder(left.orderKey, right.orderKey);
|
|
27
24
|
if (bySameSourceOrder !== 0)
|
|
28
25
|
return bySameSourceOrder;
|
|
29
26
|
}
|
|
27
|
+
else {
|
|
28
|
+
const bySameTurnPhase = compareSameTurnPhase(left, right);
|
|
29
|
+
if (bySameTurnPhase !== 0)
|
|
30
|
+
return bySameTurnPhase;
|
|
31
|
+
}
|
|
30
32
|
const byStartTime = compareOptionalIsoTime(left.startedAt, right.startedAt);
|
|
31
33
|
if (byStartTime !== 0)
|
|
32
34
|
return byStartTime;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { flattenTraceNodes, nestTraceNodes } from "./trace-nodes.js";
|
|
2
|
+
import { compareTraceOrder } from "./trace-order.js";
|
|
2
3
|
export function mergeOlderTracePage(current, older) {
|
|
3
4
|
if (current.piboSessionId !== older.piboSessionId)
|
|
4
5
|
return current;
|
|
@@ -28,8 +29,8 @@ export function mergeRefreshedTracePage(current, refreshed) {
|
|
|
28
29
|
return refreshed;
|
|
29
30
|
return {
|
|
30
31
|
...refreshed,
|
|
31
|
-
nodes:
|
|
32
|
-
rawEvents: current.rawEvents
|
|
32
|
+
nodes: mergeRefreshedTraceNodes(current.nodes, refreshed),
|
|
33
|
+
rawEvents: mergeTraceRawEvents(current.rawEvents, refreshed.rawEvents),
|
|
33
34
|
beforeCursor: current.beforeCursor,
|
|
34
35
|
firstEventSequence: current.firstEventSequence ?? refreshed.firstEventSequence,
|
|
35
36
|
nextBeforeSequence: current.nextBeforeSequence,
|
|
@@ -38,6 +39,109 @@ export function mergeRefreshedTracePage(current, refreshed) {
|
|
|
38
39
|
eventLimit: current.eventLimit ?? refreshed.eventLimit,
|
|
39
40
|
};
|
|
40
41
|
}
|
|
42
|
+
function mergeRefreshedTraceNodes(currentNodes, refreshed) {
|
|
43
|
+
const flattenedCurrentNodes = flattenTraceNodes([...currentNodes]);
|
|
44
|
+
const refreshedNodes = flattenTraceNodes([...refreshed.nodes]);
|
|
45
|
+
const refreshedIds = new Set(refreshedNodes.map((node) => node.id));
|
|
46
|
+
const canonicalTranscriptEventIds = transcriptEventIds([...flattenedCurrentNodes, ...refreshedNodes]);
|
|
47
|
+
const refreshedOrderBoundaries = earliestTraceNodeOrdersBySource(refreshedNodes);
|
|
48
|
+
const refreshedStartedAt = earliestTraceNodeTimestamp(refreshedNodes);
|
|
49
|
+
const retainedOlderNodes = flattenedCurrentNodes.filter((node) => {
|
|
50
|
+
if (refreshedIds.has(node.id))
|
|
51
|
+
return true;
|
|
52
|
+
if (node.type === "agent.turn" &&
|
|
53
|
+
node.source === "event-log" &&
|
|
54
|
+
node.eventId &&
|
|
55
|
+
canonicalTranscriptEventIds.has(node.eventId))
|
|
56
|
+
return false;
|
|
57
|
+
if (isTransientTailNode(node))
|
|
58
|
+
return false;
|
|
59
|
+
const eventSequence = node.orderKey?.eventSequence;
|
|
60
|
+
if (eventSequence !== undefined && refreshed.firstEventSequence !== undefined) {
|
|
61
|
+
return eventSequence < refreshed.firstEventSequence;
|
|
62
|
+
}
|
|
63
|
+
const refreshedOrderBoundary = node.orderKey
|
|
64
|
+
? refreshedOrderBoundaries.get(node.orderKey.sourceRank)
|
|
65
|
+
: undefined;
|
|
66
|
+
if (node.orderKey && refreshedOrderBoundary) {
|
|
67
|
+
return compareTraceOrder(node.orderKey, refreshedOrderBoundary) < 0;
|
|
68
|
+
}
|
|
69
|
+
if (node.source === "live" || node.orderKey?.streamId !== undefined)
|
|
70
|
+
return false;
|
|
71
|
+
const startedAt = parseTimestamp(node.startedAt);
|
|
72
|
+
return startedAt !== undefined && refreshedStartedAt !== undefined && startedAt < refreshedStartedAt;
|
|
73
|
+
});
|
|
74
|
+
const mergedIds = new Set([...retainedOlderNodes, ...refreshedNodes].map((node) => node.id));
|
|
75
|
+
const removedCurrentIds = new Set(flattenedCurrentNodes
|
|
76
|
+
.filter((node) => !mergedIds.has(node.id))
|
|
77
|
+
.map((node) => node.id));
|
|
78
|
+
return mergeTraceNodes(clearRemovedParentIds(retainedOlderNodes, removedCurrentIds), clearRemovedParentIds(refreshedNodes, removedCurrentIds));
|
|
79
|
+
}
|
|
80
|
+
function clearRemovedParentIds(nodes, removedIds) {
|
|
81
|
+
return nodes.map((node) => node.parentId && removedIds.has(node.parentId)
|
|
82
|
+
? { ...node, parentId: undefined }
|
|
83
|
+
: node);
|
|
84
|
+
}
|
|
85
|
+
function transcriptEventIds(nodes) {
|
|
86
|
+
const eventIds = new Set();
|
|
87
|
+
for (const node of nodes) {
|
|
88
|
+
if (node.source === "transcript" && node.eventId)
|
|
89
|
+
eventIds.add(node.eventId);
|
|
90
|
+
}
|
|
91
|
+
return eventIds;
|
|
92
|
+
}
|
|
93
|
+
function isTransientTailNode(node) {
|
|
94
|
+
return node.source === "live" || node.orderKey?.streamId !== undefined;
|
|
95
|
+
}
|
|
96
|
+
function earliestTraceNodeOrdersBySource(nodes) {
|
|
97
|
+
const earliestBySource = new Map();
|
|
98
|
+
for (const node of nodes) {
|
|
99
|
+
if (!node.orderKey)
|
|
100
|
+
continue;
|
|
101
|
+
const earliest = earliestBySource.get(node.orderKey.sourceRank);
|
|
102
|
+
if (!earliest || compareTraceOrder(node.orderKey, earliest) < 0) {
|
|
103
|
+
earliestBySource.set(node.orderKey.sourceRank, node.orderKey);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return earliestBySource;
|
|
107
|
+
}
|
|
108
|
+
function earliestTraceNodeTimestamp(nodes) {
|
|
109
|
+
let earliest;
|
|
110
|
+
for (const node of nodes) {
|
|
111
|
+
const startedAt = parseTimestamp(node.startedAt);
|
|
112
|
+
if (startedAt === undefined)
|
|
113
|
+
continue;
|
|
114
|
+
earliest = earliest === undefined ? startedAt : Math.min(earliest, startedAt);
|
|
115
|
+
}
|
|
116
|
+
return earliest;
|
|
117
|
+
}
|
|
118
|
+
function parseTimestamp(value) {
|
|
119
|
+
if (!value)
|
|
120
|
+
return undefined;
|
|
121
|
+
const parsed = Date.parse(value);
|
|
122
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
123
|
+
}
|
|
124
|
+
function mergeTraceRawEvents(currentEvents, refreshedEvents) {
|
|
125
|
+
if (!refreshedEvents.length)
|
|
126
|
+
return currentEvents;
|
|
127
|
+
const firstRefreshedSequence = refreshedEvents.reduce((first, event) => {
|
|
128
|
+
if (event.eventSequence === undefined)
|
|
129
|
+
return first;
|
|
130
|
+
return first === undefined ? event.eventSequence : Math.min(first, event.eventSequence);
|
|
131
|
+
}, undefined);
|
|
132
|
+
const byKey = new Map();
|
|
133
|
+
for (const event of currentEvents) {
|
|
134
|
+
if (firstRefreshedSequence !== undefined && event.eventSequence !== undefined && event.eventSequence >= firstRefreshedSequence)
|
|
135
|
+
continue;
|
|
136
|
+
byKey.set(traceRawEventKey(event), event);
|
|
137
|
+
}
|
|
138
|
+
for (const event of refreshedEvents)
|
|
139
|
+
byKey.set(traceRawEventKey(event), event);
|
|
140
|
+
return [...byKey.values()];
|
|
141
|
+
}
|
|
142
|
+
function traceRawEventKey(event) {
|
|
143
|
+
return event.id || `${event.eventSequence ?? ""}:${event.type}:${event.createdAt}`;
|
|
144
|
+
}
|
|
41
145
|
function mergeTraceNodes(olderNodes, currentNodes) {
|
|
42
146
|
const byId = new Map();
|
|
43
147
|
for (const node of flattenTraceNodes([...olderNodes])) {
|