@pasko70/pibo 3.1.2 → 3.1.4

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.
Files changed (31) hide show
  1. package/dist/apps/chat/data/timeline-query-service.js +5 -2
  2. package/dist/apps/chat/output-compactor.js +33 -30
  3. package/dist/apps/chat/trace-v2.js +1 -0
  4. package/dist/apps/chat/trace.js +3 -1
  5. package/dist/apps/chat/web-app.js +106 -65
  6. package/dist/apps/chat-ui/assets/{dist-W5HOqHym.js → dist-BOqsX6_s.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-Djul9BmZ.js → dist-BcUbdOKJ.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-ClUlQWYN.js → dist-DkpIJ_Pp.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-GD0JGdCi.js → dist-a0mykCz7.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-CUQJqggN.js → dist-qDZ-CdlK.js} +1 -1
  11. package/dist/apps/chat-ui/assets/index-G2ic-FSG.js +228 -0
  12. package/dist/apps/chat-ui/index.html +1 -1
  13. package/dist/apps/chat-vscode-web/assets/index-zQ1fNz5K.js +43 -0
  14. package/dist/apps/chat-vscode-web/index.html +1 -1
  15. package/dist/cli-session/localSessionSource.js +26 -20
  16. package/dist/core/output-persistence-retry.js +23 -1
  17. package/dist/core/output-render-sequence.js +33 -9
  18. package/dist/data/ingest-service.js +8 -0
  19. package/dist/debug/index.js +294 -2
  20. package/dist/debug/output-integrity.js +608 -0
  21. package/dist/debug/output-repair.js +584 -0
  22. package/dist/debug/trace.js +2 -0
  23. package/dist/reliability/store.js +2 -2
  24. package/dist/sessions/pibo-data-store.js +26 -20
  25. package/dist/shared/trace-engine.js +5 -2
  26. package/dist/shared/trace-event-projection.js +74 -0
  27. package/dist/shared/trace-page-merge.js +31 -5
  28. package/npm-shrinkwrap.json +2 -2
  29. package/package.json +1 -1
  30. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +0 -228
  31. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +0 -43
@@ -150,7 +150,7 @@ export class PiboDataSessionStore {
150
150
  claimOrAttachOutputPart(input) {
151
151
  return this.dataStore.transaction(() => {
152
152
  const indexAttribute = outputPartIndexAttribute(input.kind);
153
- const eventTypes = outputPartEventTypes(input.kind);
153
+ const eventTypes = [...outputPartEventTypes(input.kind), "message_finished"];
154
154
  const placeholders = eventTypes.map(() => "?").join(", ");
155
155
  const rows = this.db.prepare(`
156
156
  SELECT type, attributes_json
@@ -159,26 +159,33 @@ export class PiboDataSessionStore {
159
159
  ORDER BY stream_id ASC
160
160
  `).all(input.piboSessionId, input.eventId, ...eventTypes);
161
161
  let maximum = -1;
162
- const latestTypeByIndex = new Map();
162
+ let turnCompleted = false;
163
+ const persistedParts = [];
163
164
  for (const row of rows) {
165
+ if (row.type === "message_finished") {
166
+ turnCompleted = true;
167
+ continue;
168
+ }
164
169
  const attributes = parseJsonObject(row.attributes_json);
165
170
  const index = attributes[indexAttribute];
166
171
  if (typeof index !== "number" || !Number.isSafeInteger(index) || index < 0)
167
172
  continue;
168
173
  maximum = Math.max(maximum, index);
169
- if (attributes.outputPartFingerprint === input.fingerprint
170
- || attributes.identityFingerprint === input.identityFingerprint) {
171
- this.observeOutputPartIndex(input, index);
172
- return index;
173
- }
174
- latestTypeByIndex.set(index, row.type);
174
+ persistedParts.push({ index, attributes });
175
175
  }
176
- const latestOpen = [...latestTypeByIndex.entries()]
177
- .filter(([, eventType]) => !outputPartEventIsTerminal(eventType))
178
- .sort(([left], [right]) => right - left)[0]?.[0];
179
- if (latestOpen !== undefined) {
180
- this.observeOutputPartIndex(input, latestOpen);
181
- return latestOpen;
176
+ // An unfinished durable part may still belong to another process. Only a
177
+ // completed turn provides enough evidence to reattach an exact replay.
178
+ if (turnCompleted) {
179
+ const matchesReplay = ({ attributes }) => attributes.outputPartFingerprint === input.fingerprint
180
+ || attributes.identityFingerprint === input.identityFingerprint;
181
+ const replay = (input.suppliedIndex === undefined
182
+ ? undefined
183
+ : persistedParts.find((part) => part.index === input.suppliedIndex && matchesReplay(part)))
184
+ ?? persistedParts.find(matchesReplay);
185
+ if (replay) {
186
+ this.observeOutputPartIndex(input, replay.index);
187
+ return replay.index;
188
+ }
182
189
  }
183
190
  const minimum = Math.max(input.proposedIndex, maximum + 1);
184
191
  const row = this.db.prepare(`
@@ -193,6 +200,11 @@ export class PiboDataSessionStore {
193
200
  return row.part_index;
194
201
  });
195
202
  }
203
+ observeOutputPart(input) {
204
+ this.dataStore.transaction(() => {
205
+ this.observeOutputPartIndex(input, input.index);
206
+ });
207
+ }
196
208
  observeOutputPartIndex(input, index) {
197
209
  this.db.prepare(`
198
210
  INSERT INTO session_output_part_counters (
@@ -639,12 +651,6 @@ function outputPartEventTypes(kind) {
639
651
  case "compaction": return ["compaction_start", "compaction_end"];
640
652
  }
641
653
  }
642
- function outputPartEventIsTerminal(eventType) {
643
- return eventType === "assistant_message"
644
- || eventType === "thinking_finished"
645
- || eventType === "assistant_usage"
646
- || eventType === "compaction_end";
647
- }
648
654
  function parseJsonObject(json) {
649
655
  if (!json)
650
656
  return {};
@@ -1,5 +1,5 @@
1
1
  import { reconcileAsyncAgentRunStatuses } from "./trace-async-agent-runs.js";
2
- import { applySingleEventToNodes, dedupeTraceEvents, findOpenTranscriptEventIds, latestTraceStreamId, mergeMessageTurnTimings, messageTurnTimingsFromEvents, reconcileTranscriptUserMessages, } from "./trace-event-projection.js";
2
+ import { applySingleEventToNodes, dedupeTraceEvents, findOpenTranscriptEventIds, latestTraceStreamId, markIncompletePersistedTurns, mergeMessageTurnTimings, messageTurnTimingsFromEvents, reconcileTranscriptUserMessages, } from "./trace-event-projection.js";
3
3
  import { flattenTraceNodes, mapTraceNodesById, nestTraceNodes } from "./trace-nodes.js";
4
4
  import { mapTraceChildSessionsByParent, mapTraceSubagentSessionLinks, } from "./trace-subagent-links.js";
5
5
  import { projectHistoryEntries, traceNodesFromHistoryEntries } from "./trace-history.js";
@@ -18,7 +18,8 @@ export function buildTraceViewFromEvents(input) {
18
18
  const openHistoryEventIds = findOpenTranscriptEventIds(events, sessionStatus);
19
19
  const suppliedTurnTimings = input.turnTimings ?? [];
20
20
  const eventTurnTimings = messageTurnTimingsFromEvents(events);
21
- const timingOverflow = suppliedTurnTimings.length + eventTurnTimings.length > TRACE_RECONCILIATION_TIMING_CAP;
21
+ const timingOverflow = input.turnTimingOverflow === true
22
+ || suppliedTurnTimings.length + eventTurnTimings.length > TRACE_RECONCILIATION_TIMING_CAP;
22
23
  const turnTimings = timingOverflow
23
24
  ? []
24
25
  : mergeMessageTurnTimings(suppliedTurnTimings, eventTurnTimings);
@@ -42,11 +43,13 @@ export function buildTraceViewFromEvents(input) {
42
43
  for (const storedEvent of events) {
43
44
  applySingleEventToNodes(nodes, byId, input.session.id, storedEvent, childByParent, linkedChildByToolCallId, historyCoverage, openHistoryEventIds, sessionStatus);
44
45
  }
46
+ const hasIncompleteTurns = markIncompletePersistedTurns(nodes, byId, input.session.id, events, turnTimings, sessionStatus);
45
47
  const nestedNodes = nestTraceNodes(nodes);
46
48
  reconcileAsyncAgentRunStatuses(nestedNodes);
47
49
  return {
48
50
  piboSessionId: input.session.id,
49
51
  piSessionId: input.session.piSessionId,
52
+ ...(hasIncompleteTurns ? { integrityStatus: "incomplete" } : {}),
50
53
  title: input.session.title ?? "Untitled Session",
51
54
  version: "",
52
55
  latestStreamId: latestTraceStreamId(events, input.latestStreamId),
@@ -232,6 +232,80 @@ function turnClosedAt(turn, byId) {
232
232
  .at(0);
233
233
  return error?.startedAt;
234
234
  }
235
+ const INCOMPLETE_TURN_SUMMARY = "Incomplete output lifecycle";
236
+ const INCOMPLETE_TURN_ERROR = "Persisted output has message_started but no message_finished or session_error event.";
237
+ export function markIncompletePersistedTurns(nodes, byId, piboSessionId, events, turnTimings, sessionStatus) {
238
+ const lifecycleByEventId = new Map();
239
+ for (const timing of turnTimings) {
240
+ const lifecycle = lifecycleByEventId.get(timing.eventId) ?? { started: false, completed: false };
241
+ lifecycle.started ||= timing.startedAt !== undefined;
242
+ lifecycle.completed ||= timing.completedAt !== undefined;
243
+ lifecycleByEventId.set(timing.eventId, lifecycle);
244
+ }
245
+ const incompleteEventIds = new Set([...lifecycleByEventId].flatMap(([eventId, lifecycle]) => lifecycle.started && !lifecycle.completed ? [eventId] : []));
246
+ if (sessionStatus === "running") {
247
+ const currentTurn = [...turnTimings].reverse().find((timing) => timing.userMessageType !== "message_steered" &&
248
+ timing.startedAt !== undefined);
249
+ if (currentTurn?.startedAt !== undefined && currentTurn.completedAt === undefined) {
250
+ incompleteEventIds.delete(currentTurn.eventId);
251
+ }
252
+ }
253
+ if (incompleteEventIds.size === 0)
254
+ return false;
255
+ const startByEventId = new Map();
256
+ const lastByEventId = new Map();
257
+ for (const storedEvent of events) {
258
+ const event = storedEvent.payload;
259
+ const eventId = "eventId" in event && typeof event.eventId === "string" ? event.eventId : undefined;
260
+ if (!eventId || !incompleteEventIds.has(eventId))
261
+ continue;
262
+ if (event.type === "message_started" && !startByEventId.has(eventId))
263
+ startByEventId.set(eventId, storedEvent);
264
+ lastByEventId.set(eventId, storedEvent);
265
+ }
266
+ for (const eventId of incompleteEventIds) {
267
+ const start = startByEventId.get(eventId);
268
+ if (!start)
269
+ continue;
270
+ const startEvent = start.payload;
271
+ const turnId = messageTurnNodeId(eventId);
272
+ let turn = byId.get(turnId);
273
+ if (!turn) {
274
+ turn = traceNodeFromEvent(piboSessionId, startEvent, new Map(), new Map(), sessionStatus, start.createdAt, start.eventSequence, start.streamId, start.streamFrameIndex, start.traceSource, start.id);
275
+ if (!turn)
276
+ continue;
277
+ nodes.push(turn);
278
+ byId.set(turn.id, turn);
279
+ }
280
+ turn.status = "error";
281
+ turn.summary = INCOMPLETE_TURN_SUMMARY;
282
+ turn.error = INCOMPLETE_TURN_ERROR;
283
+ const markerId = `event:incomplete-turn:${eventId}`;
284
+ if (byId.has(markerId))
285
+ continue;
286
+ const last = lastByEventId.get(eventId) ?? start;
287
+ const marker = {
288
+ id: markerId,
289
+ parentId: turnId,
290
+ piboSessionId,
291
+ eventId,
292
+ type: "error",
293
+ title: "Incomplete Turn",
294
+ status: "error",
295
+ startedAt: last.createdAt,
296
+ summary: INCOMPLETE_TURN_SUMMARY,
297
+ error: INCOMPLETE_TURN_ERROR,
298
+ output: INCOMPLETE_TURN_ERROR,
299
+ source: "event-log",
300
+ stableKey: `incomplete-turn:${eventId}`,
301
+ orderKey: eventTraceNodeOrder(last.eventSequence, "session_error", last.streamId, last.streamFrameIndex, last.traceSource, undefined, last.createdAt),
302
+ children: [],
303
+ };
304
+ nodes.push(marker);
305
+ byId.set(marker.id, marker);
306
+ }
307
+ return [...incompleteEventIds].some((eventId) => byId.has(`event:incomplete-turn:${eventId}`));
308
+ }
235
309
  export function eventsCanAffectAsyncAgentRunStatus(events) {
236
310
  return events.some((event) => {
237
311
  const type = event.payload.type;
@@ -4,10 +4,12 @@ export function mergeOlderTracePage(current, older) {
4
4
  if (current.piboSessionId !== older.piboSessionId)
5
5
  return current;
6
6
  const rawEvents = mergeTraceRawEvents(older.rawEvents, current.rawEvents, false);
7
+ const nodes = mergeTraceNodes(older.nodes, current.nodes);
7
8
  return {
8
9
  ...current,
9
10
  version: current.version,
10
- nodes: mergeTraceNodes(older.nodes, current.nodes),
11
+ integrityStatus: traceIntegrityStatus(nodes) ?? current.integrityStatus ?? older.integrityStatus,
12
+ nodes,
11
13
  rawEvents,
12
14
  beforeCursor: older.beforeCursor ?? current.beforeCursor,
13
15
  firstEventSequence: older.firstEventSequence ?? current.firstEventSequence,
@@ -20,9 +22,11 @@ export function mergeOlderTracePage(current, older) {
20
22
  export function mergeRefreshedTracePage(current, refreshed) {
21
23
  if (current.piboSessionId !== refreshed.piboSessionId)
22
24
  return refreshed;
25
+ const nodes = mergeRefreshedTraceNodes(current.nodes, refreshed);
23
26
  return {
24
27
  ...refreshed,
25
- nodes: mergeRefreshedTraceNodes(current.nodes, refreshed),
28
+ integrityStatus: traceIntegrityStatus(nodes) ?? refreshed.integrityStatus,
29
+ nodes,
26
30
  rawEvents: mergeTraceRawEvents(current.rawEvents, refreshed.rawEvents, true),
27
31
  beforeCursor: current.beforeCursor,
28
32
  firstEventSequence: current.firstEventSequence ?? refreshed.firstEventSequence,
@@ -36,10 +40,13 @@ function mergeRefreshedTraceNodes(currentNodes, refreshed) {
36
40
  const flattenedCurrentNodes = flattenTraceNodes([...currentNodes]);
37
41
  const refreshedNodes = flattenTraceNodes([...refreshed.nodes]);
38
42
  const refreshedIds = new Set(refreshedNodes.map((node) => node.id));
43
+ const refreshedTerminalEventIds = terminalEventIds(refreshedNodes);
39
44
  const canonicalTranscriptEventIds = transcriptEventIds([...flattenedCurrentNodes, ...refreshedNodes]);
40
45
  const refreshedOrderBoundaries = earliestTraceNodeOrdersBySource(refreshedNodes);
41
46
  const refreshedStartedAt = earliestTraceNodeTimestamp(refreshedNodes);
42
47
  const retainedOlderNodes = flattenedCurrentNodes.filter((node) => {
48
+ if (isIncompleteTurnMarker(node) && node.eventId && refreshedTerminalEventIds.has(node.eventId))
49
+ return false;
43
50
  if (refreshedIds.has(node.id))
44
51
  return true;
45
52
  if (node.type === "agent.turn" &&
@@ -71,9 +78,28 @@ function mergeRefreshedTraceNodes(currentNodes, refreshed) {
71
78
  return mergeTraceNodes(clearRemovedParentIds(retainedOlderNodes, removedCurrentIds), clearRemovedParentIds(refreshedNodes, removedCurrentIds));
72
79
  }
73
80
  function clearRemovedParentIds(nodes, removedIds) {
74
- return nodes.map((node) => node.parentId && removedIds.has(node.parentId)
75
- ? { ...node, parentId: undefined }
76
- : node);
81
+ return nodes.map((node) => ({
82
+ ...node,
83
+ ...(node.parentId && removedIds.has(node.parentId) ? { parentId: undefined } : {}),
84
+ children: [],
85
+ }));
86
+ }
87
+ function traceIntegrityStatus(nodes) {
88
+ return flattenTraceNodes([...nodes]).some(isIncompleteTurnMarker) ? "incomplete" : undefined;
89
+ }
90
+ function isIncompleteTurnMarker(node) {
91
+ return node.id.startsWith("event:incomplete-turn:") || node.stableKey?.startsWith("incomplete-turn:") === true;
92
+ }
93
+ function terminalEventIds(nodes) {
94
+ return new Set(nodes.flatMap((node) => {
95
+ if (!node.eventId)
96
+ return [];
97
+ if (node.type === "agent.turn" && node.completedAt)
98
+ return [node.eventId];
99
+ if (node.type === "error" && node.title === "Session Error")
100
+ return [node.eventId];
101
+ return [];
102
+ }));
77
103
  }
78
104
  function transcriptEventIds(nodes) {
79
105
  const eventIds = new Set();
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "3.1.2",
9
+ "version": "3.1.4",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "packages/workflows"