@pasko70/pibo 2.5.2 → 3.0.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.
Files changed (100) hide show
  1. package/context/pibo-native-tooling.md +1 -0
  2. package/dist/agent-runtime/contract.js +2 -1
  3. package/dist/agent-runtime/history.js +53 -1
  4. package/dist/agent-runtime/routed-session.js +100 -26
  5. package/dist/agent-runtimes/codex-native/adapter.js +304 -30
  6. package/dist/agent-runtimes/codex-native/client.js +22 -3
  7. package/dist/agent-runtimes/codex-native/first-use.js +294 -0
  8. package/dist/agent-runtimes/codex-native/history.js +26 -7
  9. package/dist/agent-runtimes/codex-native/process.js +14 -0
  10. package/dist/agent-runtimes/codex-native/thread.js +59 -25
  11. package/dist/agent-runtimes/codex-native/turn.js +27 -0
  12. package/dist/agent-runtimes/history-proof.js +8 -0
  13. package/dist/agent-runtimes/omp/adapter.js +61 -14
  14. package/dist/agent-runtimes/omp/history.js +189 -34
  15. package/dist/agent-runtimes/omp/thread.js +82 -36
  16. package/dist/agent-runtimes/pi/adapter.js +30 -3
  17. package/dist/agent-runtimes/pi/history.js +66 -5
  18. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  19. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  20. package/dist/apps/chat/chat-api-routes.js +1 -1
  21. package/dist/apps/chat/chat-files.js +130 -9
  22. package/dist/apps/chat/chat-settings-routes.js +27 -2
  23. package/dist/apps/chat/chat-speech.js +100 -0
  24. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  25. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  26. package/dist/apps/chat/trace-v2.js +210 -8
  27. package/dist/apps/chat/trace.js +3 -0
  28. package/dist/apps/chat/web-app.js +108 -3
  29. package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  38. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  39. package/dist/apps/chat-vscode-web/index.html +2 -2
  40. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  41. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
  42. package/dist/cli.js +49 -5
  43. package/dist/config/config.js +59 -8
  44. package/dist/core/preview-server-settings.js +20 -0
  45. package/dist/core/session-router.js +464 -37
  46. package/dist/core/user-settings.js +10 -0
  47. package/dist/data/payload-store.js +57 -1
  48. package/dist/data/schema.js +18 -1
  49. package/dist/debug/trace.js +5 -0
  50. package/dist/gateway/client.js +368 -65
  51. package/dist/gateway/protocol.js +2 -0
  52. package/dist/gateway/server.js +8 -0
  53. package/dist/gateway/web.js +2 -0
  54. package/dist/index.js +1 -1
  55. package/dist/loops/store.js +6 -4
  56. package/dist/mcp/config.js +89 -17
  57. package/dist/mcp/daemon-client.js +473 -153
  58. package/dist/mcp/daemon.js +401 -37
  59. package/dist/plugins/builtin.js +2 -2
  60. package/dist/plugins/codex-native.js +2 -0
  61. package/dist/plugins/registry.js +259 -1
  62. package/dist/previews/cli.js +345 -0
  63. package/dist/previews/config.js +61 -0
  64. package/dist/previews/manager.js +644 -0
  65. package/dist/previews/network.js +198 -0
  66. package/dist/previews/plugin.js +11 -0
  67. package/dist/previews/process-supervisor.js +38 -0
  68. package/dist/previews/proxy.js +387 -0
  69. package/dist/previews/store.js +750 -0
  70. package/dist/previews/types.js +1 -0
  71. package/dist/previews/web-app.js +446 -0
  72. package/dist/runs/registry.js +22 -0
  73. package/dist/runs/tools.js +46 -7
  74. package/dist/session-ui/terminalRows.js +187 -13
  75. package/dist/sessions/pibo-data-store.js +37 -0
  76. package/dist/sessions/runtime-binding-persistence.js +34 -0
  77. package/dist/sessions/runtime-binding.js +6 -0
  78. package/dist/sessions/sqlite-store.js +37 -0
  79. package/dist/shared/trace-engine.js +12 -64
  80. package/dist/shared/trace-event-projection.js +16 -0
  81. package/dist/shared/trace-history.js +684 -184
  82. package/dist/shared/trace-limits.js +7 -0
  83. package/dist/shared/trace-live-patch.js +61 -0
  84. package/dist/shared/trace-tool-identity.js +43 -0
  85. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  86. package/dist/speech/openai-codex.js +407 -0
  87. package/dist/speech/types.js +8 -0
  88. package/dist/subagents/context.js +5 -3
  89. package/dist/subagents/observations.js +94 -6
  90. package/dist/subagents/tool.js +103 -20
  91. package/dist/tools/codex-image-artifacts.js +24 -0
  92. package/dist/tools/codex-image-generation.js +4 -22
  93. package/dist/web/channel.js +88 -4
  94. package/dist/web/http.js +2 -1
  95. package/npm-shrinkwrap.json +2 -2
  96. package/package.json +1 -1
  97. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  98. package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
  99. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  100. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
@@ -1,13 +1,18 @@
1
+ import { historyReconciliationDigest, historyReconciliationEntrySignature, } from "../agent-runtime/history.js";
1
2
  import { historyTraceOrder } from "./trace-order.js";
2
3
  import { attachAsyncAgentRunNode, reconcileAsyncAgentRunStatuses } from "./trace-async-agent-runs.js";
3
4
  import { sortTraceNodes } from "./trace-nodes.js";
4
5
  import { createRunNotificationNode, parseRunNotificationText } from "./trace-run-notifications.js";
5
6
  import { isSubagentToolName } from "./trace-subagent-links.js";
6
7
  import { assistantMessageNodeId, messageTurnNodeId, thinkingNodeId } from "./trace-event-projection.js";
7
- export function projectHistoryEntries(entries, sessionStatus, openHistoryEventIds, turnTimings = []) {
8
+ import { TRACE_RECONCILIATION_ENTRY_CAP, TRACE_RECONCILIATION_TIMING_CAP } from "./trace-limits.js";
9
+ import { qualifiedHistoryToolNodeId } from "./trace-tool-identity.js";
10
+ const MAX_CONFIDENT_TIMESTAMP_DISTANCE_MS = 5 * 60 * 1_000;
11
+ const PERSISTED_TIMESTAMP_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|([+-])(\d{2}):(\d{2}))$/;
12
+ export function projectHistoryEntries(entries, sessionStatus, openHistoryEventIds, turnTimings = [], reconciliationProof, reconciliationAuthoritative = false) {
8
13
  if (sessionStatus !== "running" || openHistoryEventIds.size === 0)
9
14
  return [...entries];
10
- const userTimingAssignments = assignHistoryUserTimings(entries, turnTimings);
15
+ const { userAssignments, userFallbackEventIds } = assignHistoryTimings(entries, turnTimings, reconciliationProof, reconciliationAuthoritative);
11
16
  let lastUserMessageIndex = -1;
12
17
  let lastUserEventId;
13
18
  for (let index = 0; index < entries.length; index += 1) {
@@ -15,32 +20,42 @@ export function projectHistoryEntries(entries, sessionStatus, openHistoryEventId
15
20
  if (entry.type !== "message" || entry.role !== "user")
16
21
  continue;
17
22
  lastUserMessageIndex = index;
18
- lastUserEventId = entry.turnId && openHistoryEventIds.has(entry.turnId)
19
- ? entry.turnId
20
- : userTimingAssignments.get(index)?.eventId;
23
+ lastUserEventId = canonicalProductTurnId(entry, userAssignments.get(index), userFallbackEventIds.get(index));
21
24
  }
22
25
  return lastUserMessageIndex !== -1 && lastUserEventId && openHistoryEventIds.has(lastUserEventId)
23
26
  ? entries.slice(0, lastUserMessageIndex)
24
27
  : [...entries];
25
28
  }
26
- export function traceNodesFromHistoryEntries(piboSessionId, entries, turnTimings = []) {
29
+ export function traceNodesFromHistoryEntries(piboSessionId, entries, turnTimings = [], reconciliationProof, reconciliationAuthoritative = false) {
27
30
  const nodes = [];
28
- const turnTimingAssignments = assignHistoryTurnTimings(entries, turnTimings);
29
- const userTimingAssignments = assignHistoryUserTimings(entries, turnTimings);
31
+ const { userAssignments, turnAssignments, userFallbackEventIds, turnFallbackEventIds, turnProjectionStates } = assignHistoryTimings(entries, turnTimings, reconciliationProof, reconciliationAuthoritative);
32
+ const qualifiedNativeToolCallIds = nativeToolCallIdsRequiringQualification(entries, reconciliationProof);
33
+ const projectionStateByEventId = new Map();
30
34
  let assistantTurnIndex = 0;
31
35
  for (let index = 0; index < entries.length; index += 1) {
32
36
  const entry = entries[index];
33
37
  if (entry.type === "message") {
34
38
  if (entry.role === "user") {
35
- nodes.push(createUserMessageNode(piboSessionId, entry, index, userTimingAssignments.get(index)));
39
+ nodes.push(createUserMessageNode(piboSessionId, entry, index, userAssignments.get(index), userFallbackEventIds.get(index)));
36
40
  }
37
41
  else if (entry.role === "assistant" || entry.role === "tool") {
38
42
  const turn = collectAssistantTurn(entries, index);
39
- const hasAssistant = turn.entries.some(({ entry: turnEntry }) => turnEntry.role === "assistant");
40
- const timing = hasAssistant ? turnTimingAssignments[assistantTurnIndex] : undefined;
41
- nodes.push(...createAssistantTurnNodes(piboSessionId, turn.entries, timing));
42
- if (hasAssistant)
43
- assistantTurnIndex += 1;
43
+ const timing = turnAssignments[assistantTurnIndex];
44
+ const fallbackEventId = turnFallbackEventIds.get(assistantTurnIndex);
45
+ const identityEntry = turn.entries.find(({ entry: turnEntry }) => turnEntry.role === "assistant")
46
+ ?? turn.entries[0];
47
+ const eventId = identityEntry
48
+ ? canonicalProductTurnId(identityEntry.entry, timing, fallbackEventId)
49
+ : undefined;
50
+ const projectionState = eventId
51
+ ? turnProjectionStates.get(assistantTurnIndex)
52
+ ?? projectionStateByEventId.get(eventId)
53
+ ?? emptyAssistantTurnProjectionState()
54
+ : emptyAssistantTurnProjectionState();
55
+ nodes.push(...createAssistantTurnNodes(piboSessionId, turn.entries, timing, projectionState, fallbackEventId, qualifiedNativeToolCallIds));
56
+ if (eventId)
57
+ projectionStateByEventId.set(eventId, projectionState);
58
+ assistantTurnIndex += 1;
44
59
  index = turn.nextIndex - 1;
45
60
  }
46
61
  }
@@ -71,7 +86,8 @@ function collectHistoryTurnTimingTargets(entries) {
71
86
  const targets = [];
72
87
  let latestUserText;
73
88
  let latestUserEntryIndex;
74
- let latestTurnId;
89
+ let latestUserAt;
90
+ let latestProviderNativeTurnId;
75
91
  for (let index = 0; index < entries.length; index += 1) {
76
92
  const entry = entries[index];
77
93
  if (entry.type !== "message")
@@ -79,180 +95,538 @@ function collectHistoryTurnTimingTargets(entries) {
79
95
  if (entry.role === "user") {
80
96
  latestUserText = normalizedPrompt(historyMessageText(entry));
81
97
  latestUserEntryIndex = index;
82
- latestTurnId = entry.turnId;
98
+ latestUserAt = parsedTimestamp(entry.createdAt);
99
+ latestProviderNativeTurnId = entry.nativeTurnId;
83
100
  continue;
84
101
  }
85
102
  if (entry.role !== "assistant" && entry.role !== "tool")
86
103
  continue;
87
104
  const turn = collectAssistantTurn(entries, index);
88
105
  const lastAssistant = [...turn.entries].reverse().find(({ entry: turnEntry }) => turnEntry.role === "assistant");
89
- if (lastAssistant) {
90
- targets.push({
91
- prompt: latestUserText,
92
- userEntryIndex: latestUserEntryIndex,
93
- assistantAt: parsedTimestamp(lastAssistant.entry.createdAt),
94
- settled: isSettledAssistantEntry(lastAssistant.entry),
95
- turnId: lastAssistant.entry.turnId ?? latestTurnId,
96
- });
97
- }
106
+ const identityEntry = lastAssistant ?? turn.entries[0];
107
+ targets.push({
108
+ prompt: latestUserText,
109
+ userEntryIndex: latestUserEntryIndex,
110
+ userAt: latestUserAt,
111
+ assistantAt: lastAssistant ? parsedTimestamp(lastAssistant.entry.createdAt) : undefined,
112
+ providerNativeTurnId: identityEntry?.entry.nativeTurnId ?? latestProviderNativeTurnId,
113
+ entryIndexes: turn.entries.map(({ index: entryIndex }) => entryIndex),
114
+ });
98
115
  index = turn.nextIndex - 1;
99
116
  }
100
117
  return targets;
101
118
  }
102
- function assignHistoryTurnTimings(entries, turnTimings) {
103
- const messageTurnTimings = turnTimings.filter((timing) => timing.userMessageType !== "message_steered");
104
- const timingByEventId = new Map(messageTurnTimings.map((timing) => [timing.eventId, timing]));
105
- const historyTurns = collectHistoryTurnTimingTargets(entries);
106
- const assignments = historyTurns.map((turn) => turn.turnId ? timingByEventId.get(turn.turnId) : undefined);
107
- let timingCursor = messageTurnTimings.length - 1;
108
- for (let turnIndex = historyTurns.length - 1; turnIndex >= 0; turnIndex -= 1) {
109
- if (assignments[turnIndex]) {
110
- const assignedIndex = messageTurnTimings.indexOf(assignments[turnIndex]);
111
- if (assignedIndex >= 0)
112
- timingCursor = Math.min(timingCursor, assignedIndex - 1);
113
- continue;
114
- }
115
- const historyTurn = historyTurns[turnIndex];
116
- if (!historyTurn?.prompt)
117
- continue;
118
- let matchedIndex;
119
- let matchedDistance = Number.POSITIVE_INFINITY;
120
- for (let timingIndex = timingCursor; timingIndex >= 0; timingIndex -= 1) {
121
- const timing = messageTurnTimings[timingIndex];
122
- if (normalizedPrompt(timing?.userText) !== historyTurn.prompt)
123
- continue;
124
- const completedAt = parsedTimestamp(timing?.completedAt);
125
- const distance = completedAt === undefined || historyTurn.assistantAt === undefined
126
- ? Number.POSITIVE_INFINITY
127
- : Math.abs(completedAt - historyTurn.assistantAt);
128
- if (matchedIndex === undefined || distance < matchedDistance) {
129
- matchedIndex = timingIndex;
130
- matchedDistance = distance;
131
- }
132
- }
133
- if (matchedIndex === undefined)
134
- continue;
135
- assignments[turnIndex] = messageTurnTimings[matchedIndex];
136
- timingCursor = matchedIndex - 1;
137
- }
138
- return assignments;
119
+ function assignHistoryTimings(entries, turnTimings, reconciliationProof, reconciliationAuthoritative = false) {
120
+ // Direct callers hand us a complete in-memory history. Paged production
121
+ // callers must provide an explicit adapter proof: complete proof decisions
122
+ // are computed once over the whole bounded scope and then projected back by
123
+ // stable position; incomplete proof can never authorize a product identity.
124
+ if (entries.length > TRACE_RECONCILIATION_ENTRY_CAP
125
+ || turnTimings.length > TRACE_RECONCILIATION_TIMING_CAP
126
+ || (reconciliationProof?.entries.length ?? 0) > TRACE_RECONCILIATION_ENTRY_CAP)
127
+ return assignHistoryTimingsWithinScope(entries, [], true);
128
+ if (!reconciliationProof)
129
+ return assignHistoryTimingsWithinScope(entries, turnTimings);
130
+ if (!reconciliationProof.complete
131
+ || !structuralClaimMatchesPage(entries, reconciliationProof))
132
+ return assignHistoryTimingsWithinScope(entries, [], true);
133
+ // A compatibility claim may carry bounded structural context, but it cannot
134
+ // assign product ownership to native output. User-only history has no output
135
+ // ownership to transfer, so its independently persisted timing evidence may
136
+ // still reconcile the user messages exactly as the direct-history path does.
137
+ const userOnlyClaim = reconciliationProof.entries.every((entry) => entry.type !== "message" || entry.role === "user");
138
+ return assignmentsFromCompleteProof(entries, reconciliationAuthoritative || userOnlyClaim ? turnTimings : [], reconciliationProof.entries, !reconciliationAuthoritative && !userOnlyClaim);
139
139
  }
140
- function assignHistoryUserTimings(entries, turnTimings) {
140
+ function assignHistoryTimingsWithinScope(entries, turnTimings, failClosed = false) {
141
141
  const users = [];
142
- const userPositionByEntryIndex = new Map();
143
142
  for (let entryIndex = 0; entryIndex < entries.length; entryIndex += 1) {
144
143
  const entry = entries[entryIndex];
145
144
  if (entry.type !== "message" || entry.role !== "user")
146
145
  continue;
147
- userPositionByEntryIndex.set(entryIndex, users.length);
148
146
  users.push({
149
147
  entryIndex,
150
148
  entryId: entry.nativeEntryId,
151
149
  turnId: entry.turnId,
152
150
  prompt: normalizedPrompt(historyMessageText(entry)),
151
+ userAt: parsedTimestamp(entry.createdAt),
153
152
  });
154
153
  }
155
- if (!users.length || !turnTimings.length)
156
- return new Map();
157
- const timingIndexByEventId = new Map(turnTimings.map((timing, timingIndex) => [timing.eventId, timingIndex]));
158
- const anchorByUserPosition = new Map();
159
- const assignedTimingIndexes = new Set();
160
- for (let userPosition = 0; userPosition < users.length; userPosition += 1) {
161
- const user = users[userPosition];
162
- const timingIndex = timingIndexByEventId.get(user.turnId ?? user.entryId ?? "");
163
- if (timingIndex === undefined || assignedTimingIndexes.has(timingIndex))
154
+ const historyTurns = collectHistoryTurnTimingTargets(entries);
155
+ const groups = collectHistoryTurnGroups(entries, users, historyTurns);
156
+ if (failClosed) {
157
+ const userFallbackEventIds = new Map();
158
+ const turnFallbackEventIds = new Map();
159
+ for (const group of groups) {
160
+ if (group.userEntryIndex !== undefined)
161
+ userFallbackEventIds.set(group.userEntryIndex, group.fallbackEventId);
162
+ for (const turnIndex of group.assistantTurnIndexes)
163
+ turnFallbackEventIds.set(turnIndex, group.fallbackEventId);
164
+ }
165
+ return {
166
+ userAssignments: new Map(),
167
+ turnAssignments: historyTurns.map(() => undefined),
168
+ userFallbackEventIds,
169
+ turnFallbackEventIds,
170
+ turnProjectionStates: new Map(),
171
+ };
172
+ }
173
+ for (const historyTurn of historyTurns) {
174
+ if (historyTurn.userEntryIndex === undefined || historyTurn.assistantAt === undefined)
164
175
  continue;
165
- anchorByUserPosition.set(userPosition, timingIndex);
166
- assignedTimingIndexes.add(timingIndex);
176
+ const user = users.find((candidate) => candidate.entryIndex === historyTurn.userEntryIndex);
177
+ if (user)
178
+ user.assistantAt ??= historyTurn.assistantAt;
167
179
  }
168
- const historyTurns = collectHistoryTurnTimingTargets(entries);
169
- let timingUpperBound = turnTimings.length - 1;
170
- for (let turnIndex = historyTurns.length - 1; turnIndex >= 0; turnIndex -= 1) {
171
- const historyTurn = historyTurns[turnIndex];
172
- const userPosition = historyTurn?.userEntryIndex === undefined
173
- ? undefined
174
- : userPositionByEntryIndex.get(historyTurn.userEntryIndex);
175
- if (!historyTurn?.prompt || userPosition === undefined)
180
+ const matches = users.map((user) => findConfidentTimingMatch(user, turnTimings));
181
+ const invalidUserPositions = invalidHistoryUserMatchPositions(matches);
182
+ const messageTurnTimings = turnTimings.filter((timing) => timing.userMessageType !== "message_steered");
183
+ const timingByEventId = new Map(messageTurnTimings.map((timing) => [timing.eventId, timing]));
184
+ const conflictingGroupIndexes = conflictingHistoryGroupIndexes(groups, matches, timingByEventId, invalidUserPositions);
185
+ const repeatedProviderTurnIds = repeatedProviderTurnIdsAcrossGroups(entries, groups);
186
+ const userAssignments = new Map();
187
+ const turnAssignments = historyTurns.map(() => undefined);
188
+ const userFallbackEventIds = new Map();
189
+ const turnFallbackEventIds = new Map();
190
+ const turnProjectionStates = new Map();
191
+ for (const group of groups) {
192
+ const userPosition = group.userPosition;
193
+ const match = userPosition === undefined ? undefined : matches[userPosition];
194
+ if (conflictingGroupIndexes.has(group.index)) {
195
+ if (group.userEntryIndex !== undefined)
196
+ userFallbackEventIds.set(group.userEntryIndex, group.fallbackEventId);
197
+ for (const turnIndex of group.assistantTurnIndexes) {
198
+ turnFallbackEventIds.set(turnIndex, group.fallbackEventId);
199
+ }
176
200
  continue;
177
- const exactAnchor = anchorByUserPosition.get(userPosition);
178
- if (exactAnchor !== undefined) {
179
- timingUpperBound = Math.min(timingUpperBound, exactAnchor - 1);
201
+ }
202
+ if (!match || userPosition === undefined || invalidUserPositions.has(userPosition)) {
203
+ if (groupNeedsStructuralFallback(entries, group, repeatedProviderTurnIds)) {
204
+ if (group.userEntryIndex !== undefined)
205
+ userFallbackEventIds.set(group.userEntryIndex, group.fallbackEventId);
206
+ for (const turnIndex of group.assistantTurnIndexes)
207
+ turnFallbackEventIds.set(turnIndex, group.fallbackEventId);
208
+ }
180
209
  continue;
181
210
  }
182
- let steeringTimingIndex;
183
- for (let timingIndex = timingUpperBound; timingIndex >= 0; timingIndex -= 1) {
184
- const timing = turnTimings[timingIndex];
185
- if (assignedTimingIndexes.has(timingIndex) || timing.userMessageType !== "message_steered")
186
- continue;
187
- if (normalizedPrompt(timing.userText) !== historyTurn.prompt)
188
- continue;
189
- steeringTimingIndex = timingIndex;
190
- break;
211
+ if (group.userEntryIndex !== undefined)
212
+ userAssignments.set(group.userEntryIndex, match.timing);
213
+ const outputTiming = outputTimingForUserTiming(match.timing, timingByEventId);
214
+ if (!outputTiming)
215
+ continue;
216
+ for (const turnIndex of group.assistantTurnIndexes)
217
+ turnAssignments[turnIndex] = outputTiming;
218
+ }
219
+ return { userAssignments, turnAssignments, userFallbackEventIds, turnFallbackEventIds, turnProjectionStates };
220
+ }
221
+ function assignmentsFromCompleteProof(entries, turnTimings, proofEntries, proofFailClosed = false) {
222
+ if (!hasUniqueStableHistoryPositions(entries)) {
223
+ return assignHistoryTimingsWithinScope(entries, turnTimings, true);
224
+ }
225
+ const proofAssignments = assignHistoryTimingsWithinScope(proofEntries, turnTimings, proofFailClosed);
226
+ const localFallbackEventIds = historyGroupFallbackEventIds(entries);
227
+ const decisionsByPosition = new Map();
228
+ for (let entryIndex = 0; entryIndex < proofEntries.length; entryIndex += 1) {
229
+ const entry = proofEntries[entryIndex];
230
+ if (entry.type !== "message" || entry.role !== "user")
231
+ continue;
232
+ decisionsByPosition.set(entry.historyPosition, {
233
+ timing: proofAssignments.userAssignments.get(entryIndex),
234
+ fallbackEventId: proofAssignments.userFallbackEventIds.get(entryIndex),
235
+ });
236
+ }
237
+ const proofTurns = collectHistoryTurnTimingTargets(proofEntries);
238
+ for (let turnIndex = 0; turnIndex < proofTurns.length; turnIndex += 1) {
239
+ const decision = {
240
+ timing: proofAssignments.turnAssignments[turnIndex],
241
+ fallbackEventId: proofAssignments.turnFallbackEventIds.get(turnIndex),
242
+ };
243
+ for (const entryIndex of proofTurns[turnIndex].entryIndexes) {
244
+ decisionsByPosition.set(proofEntries[entryIndex].historyPosition, decision);
191
245
  }
192
- if (steeringTimingIndex !== undefined) {
193
- anchorByUserPosition.set(userPosition, steeringTimingIndex);
194
- assignedTimingIndexes.add(steeringTimingIndex);
195
- timingUpperBound = steeringTimingIndex - 1;
246
+ }
247
+ const userAssignments = new Map();
248
+ const userFallbackEventIds = new Map();
249
+ for (let entryIndex = 0; entryIndex < entries.length; entryIndex += 1) {
250
+ const entry = entries[entryIndex];
251
+ if (entry.type !== "message" || entry.role !== "user")
196
252
  continue;
253
+ const decision = decisionsByPosition.get(entry.historyPosition);
254
+ if (decision?.timing)
255
+ userAssignments.set(entryIndex, decision.timing);
256
+ if (decision?.fallbackEventId)
257
+ userFallbackEventIds.set(entryIndex, decision.fallbackEventId);
258
+ if (!decision)
259
+ userFallbackEventIds.set(entryIndex, localFallbackEventIds[entryIndex]);
260
+ }
261
+ const localTurns = collectHistoryTurnTimingTargets(entries);
262
+ const turnAssignments = localTurns.map(() => undefined);
263
+ const turnFallbackEventIds = new Map();
264
+ const turnProjectionStates = projectionStatesFromCompleteProof(entries, localTurns, proofEntries, proofTurns, proofAssignments);
265
+ for (let turnIndex = 0; turnIndex < localTurns.length; turnIndex += 1) {
266
+ const turn = localTurns[turnIndex];
267
+ const decisions = turn.entryIndexes.map((entryIndex) => decisionsByPosition.get(entries[entryIndex].historyPosition));
268
+ const decision = oneConsistentProofDecision(decisions);
269
+ if (decision?.timing)
270
+ turnAssignments[turnIndex] = decision.timing;
271
+ if (decision?.fallbackEventId) {
272
+ turnFallbackEventIds.set(turnIndex, decision.fallbackEventId);
273
+ }
274
+ else if (!decision) {
275
+ const firstEntryIndex = turn.entryIndexes[0];
276
+ turnFallbackEventIds.set(turnIndex, localFallbackEventIds[firstEntryIndex]);
197
277
  }
198
- if (!historyTurn.settled)
278
+ }
279
+ return { userAssignments, turnAssignments, userFallbackEventIds, turnFallbackEventIds, turnProjectionStates };
280
+ }
281
+ function structuralClaimMatchesPage(entries, proof) {
282
+ if (!proof.fullScope
283
+ || !Number.isSafeInteger(proof.fullScope.entryCount)
284
+ || proof.fullScope.entryCount < 0
285
+ || proof.fullScope.entryCount > TRACE_RECONCILIATION_ENTRY_CAP
286
+ || proof.fullScope.entryCount !== proof.entries.length
287
+ || !/^[a-f0-9]{64}$/.test(proof.fullScope.digest)
288
+ || proof.fullScope.digest !== historyReconciliationDigest(proof.entries))
289
+ return false;
290
+ if (!hasUniqueStableHistoryPositions(entries) || !hasUniqueStableHistoryPositions(proof.entries))
291
+ return false;
292
+ if (proof.scopeId) {
293
+ if (entries.some((entry) => entry.historyScopeId !== proof.scopeId))
294
+ return false;
295
+ if (proof.entries.some((entry) => entry.historyScopeId !== proof.scopeId))
296
+ return false;
297
+ }
298
+ const proofByPosition = new Map(proof.entries.map((entry) => [entry.historyPosition, entry]));
299
+ return entries.every((entry) => {
300
+ const proofEntry = proofByPosition.get(entry.historyPosition);
301
+ return proofEntry !== undefined
302
+ && historyReconciliationEntrySignature(entry) === historyReconciliationEntrySignature(proofEntry);
303
+ });
304
+ }
305
+ function projectionStatesFromCompleteProof(entries, localTurns, proofEntries, proofTurns, proofAssignments) {
306
+ const stateByEventId = new Map();
307
+ const stateByPosition = new Map();
308
+ for (let turnIndex = 0; turnIndex < proofTurns.length; turnIndex += 1) {
309
+ const turn = proofTurns[turnIndex];
310
+ const firstAssistantIndex = turn.entryIndexes.find((entryIndex) => {
311
+ const entry = proofEntries[entryIndex];
312
+ return entry?.type === "message" && entry.role === "assistant";
313
+ });
314
+ const identityEntryIndex = firstAssistantIndex ?? turn.entryIndexes[0];
315
+ if (identityEntryIndex === undefined)
316
+ continue;
317
+ const identityEntry = proofEntries[identityEntryIndex];
318
+ if (!identityEntry || identityEntry.type !== "message")
199
319
  continue;
200
- let matchedTimingIndex;
201
- let matchedDistance = Number.POSITIVE_INFINITY;
202
- for (let timingIndex = timingUpperBound; timingIndex >= 0; timingIndex -= 1) {
203
- const timing = turnTimings[timingIndex];
204
- if (assignedTimingIndexes.has(timingIndex) || timing.userMessageType === "message_steered" || !timing.completedAt)
320
+ const eventId = canonicalProductTurnId(identityEntry, proofAssignments.turnAssignments[turnIndex], proofAssignments.turnFallbackEventIds.get(turnIndex));
321
+ const state = eventId
322
+ ? stateByEventId.get(eventId) ?? emptyAssistantTurnProjectionState()
323
+ : emptyAssistantTurnProjectionState();
324
+ for (const entryIndex of turn.entryIndexes) {
325
+ const entry = proofEntries[entryIndex];
326
+ if (!entry || entry.type !== "message" || !entry.historyPosition)
205
327
  continue;
206
- if (normalizedPrompt(timing.userText) !== historyTurn.prompt)
328
+ stateByPosition.set(entry.historyPosition, cloneAssistantTurnProjectionState(state));
329
+ advanceAssistantTurnProjectionState(state, entry);
330
+ }
331
+ if (eventId)
332
+ stateByEventId.set(eventId, state);
333
+ }
334
+ const localStates = new Map();
335
+ for (let turnIndex = 0; turnIndex < localTurns.length; turnIndex += 1) {
336
+ const firstEntryIndex = localTurns[turnIndex].entryIndexes[0];
337
+ if (firstEntryIndex === undefined)
338
+ continue;
339
+ const position = entries[firstEntryIndex]?.historyPosition;
340
+ const state = position ? stateByPosition.get(position) : undefined;
341
+ if (state)
342
+ localStates.set(turnIndex, cloneAssistantTurnProjectionState(state));
343
+ }
344
+ return localStates;
345
+ }
346
+ function emptyAssistantTurnProjectionState() {
347
+ return {
348
+ assistantPartOrdinal: 0,
349
+ reasoningPartOrdinal: 0,
350
+ toolCallCounts: new Map(),
351
+ pendingToolCallOrdinals: new Map(),
352
+ };
353
+ }
354
+ function cloneAssistantTurnProjectionState(state) {
355
+ return {
356
+ assistantPartOrdinal: state.assistantPartOrdinal,
357
+ reasoningPartOrdinal: state.reasoningPartOrdinal,
358
+ toolCallCounts: new Map(state.toolCallCounts),
359
+ pendingToolCallOrdinals: new Map([...state.pendingToolCallOrdinals].map(([toolCallId, ordinals]) => [toolCallId, [...ordinals]])),
360
+ };
361
+ }
362
+ function advanceAssistantTurnProjectionState(state, entry) {
363
+ if (entry.role === "tool" && entry.toolCallId) {
364
+ consumeToolCallOrdinal(state, entry.toolCallId);
365
+ return;
366
+ }
367
+ if (entry.role !== "assistant")
368
+ return;
369
+ const parts = historyMessageParts(entry);
370
+ for (const part of parts) {
371
+ if (part.type === "reasoning" && hasVisibleText(part.text))
372
+ state.reasoningPartOrdinal += 1;
373
+ if (part.type === "tool_call")
374
+ reserveToolCallOrdinal(state, part.toolCallId);
375
+ }
376
+ if (parts.some((part) => part.type === "text" && part.text !== ""))
377
+ state.assistantPartOrdinal += 1;
378
+ }
379
+ function reserveToolCallOrdinal(state, toolCallId) {
380
+ const ordinal = state.toolCallCounts.get(toolCallId) ?? 0;
381
+ state.toolCallCounts.set(toolCallId, ordinal + 1);
382
+ const pending = state.pendingToolCallOrdinals.get(toolCallId) ?? [];
383
+ pending.push(ordinal);
384
+ state.pendingToolCallOrdinals.set(toolCallId, pending);
385
+ return ordinal;
386
+ }
387
+ function consumeToolCallOrdinal(state, toolCallId) {
388
+ const pending = state.pendingToolCallOrdinals.get(toolCallId);
389
+ const ordinal = pending?.shift();
390
+ if (pending?.length === 0)
391
+ state.pendingToolCallOrdinals.delete(toolCallId);
392
+ if (ordinal !== undefined)
393
+ return ordinal;
394
+ const nextOrdinal = state.toolCallCounts.get(toolCallId) ?? 0;
395
+ state.toolCallCounts.set(toolCallId, nextOrdinal + 1);
396
+ return nextOrdinal;
397
+ }
398
+ function hasUniqueStableHistoryPositions(entries) {
399
+ const positions = new Set();
400
+ for (const entry of entries) {
401
+ if (!entry.historyPosition || positions.has(entry.historyPosition))
402
+ return false;
403
+ positions.add(entry.historyPosition);
404
+ }
405
+ return true;
406
+ }
407
+ function oneConsistentProofDecision(decisions) {
408
+ const first = decisions[0];
409
+ if (!first || decisions.some((decision) => !decision))
410
+ return undefined;
411
+ const firstTimingId = first.timing?.eventId;
412
+ return decisions.every((decision) => decision?.timing?.eventId === firstTimingId
413
+ && decision?.fallbackEventId === first.fallbackEventId) ? first : undefined;
414
+ }
415
+ function invalidHistoryUserMatchPositions(matches) {
416
+ const invalidUserPositions = new Set();
417
+ for (let leftPosition = 0; leftPosition < matches.length; leftPosition += 1) {
418
+ const left = matches[leftPosition];
419
+ if (!left)
420
+ continue;
421
+ for (let rightPosition = leftPosition + 1; rightPosition < matches.length; rightPosition += 1) {
422
+ const right = matches[rightPosition];
423
+ if (!right || left.timingIndex < right.timingIndex)
207
424
  continue;
208
- const completedAt = parsedTimestamp(timing.completedAt);
209
- const distance = completedAt === undefined || historyTurn.assistantAt === undefined
210
- ? Number.POSITIVE_INFINITY
211
- : Math.abs(completedAt - historyTurn.assistantAt);
212
- if (matchedTimingIndex === undefined || distance < matchedDistance) {
213
- matchedTimingIndex = timingIndex;
214
- matchedDistance = distance;
215
- }
425
+ if (left.confidence !== "identity")
426
+ invalidUserPositions.add(leftPosition);
427
+ if (right.confidence !== "identity")
428
+ invalidUserPositions.add(rightPosition);
216
429
  }
217
- if (matchedTimingIndex === undefined)
430
+ }
431
+ return invalidUserPositions;
432
+ }
433
+ function collectHistoryTurnGroups(entries, users, historyTurns) {
434
+ const fallbackEventIds = historyGroupFallbackEventIds(entries);
435
+ const groups = users.map((user, userPosition) => {
436
+ const entry = entries[user.entryIndex];
437
+ return {
438
+ index: userPosition,
439
+ userPosition,
440
+ userEntryIndex: user.entryIndex,
441
+ assistantTurnIndexes: [],
442
+ entryIndexes: [user.entryIndex],
443
+ providerNativeTurnIds: new Set(entry.type === "message" && entry.nativeTurnId ? [entry.nativeTurnId] : []),
444
+ fallbackEventId: fallbackEventIds[user.entryIndex],
445
+ };
446
+ });
447
+ const groupByUserEntryIndex = new Map(groups.map((group) => [group.userEntryIndex, group]));
448
+ for (let turnIndex = 0; turnIndex < historyTurns.length; turnIndex += 1) {
449
+ const userEntryIndex = historyTurns[turnIndex].userEntryIndex;
450
+ const group = userEntryIndex === undefined ? undefined : groupByUserEntryIndex.get(userEntryIndex);
451
+ if (!group) {
452
+ const firstEntryIndex = historyTurns[turnIndex].entryIndexes[0];
453
+ const firstEntry = firstEntryIndex === undefined ? undefined : entries[firstEntryIndex];
454
+ if (!firstEntry)
455
+ continue;
456
+ groups.push({
457
+ index: groups.length,
458
+ assistantTurnIndexes: [turnIndex],
459
+ entryIndexes: [...historyTurns[turnIndex].entryIndexes],
460
+ providerNativeTurnIds: new Set(historyTurns[turnIndex].providerNativeTurnId
461
+ ? [historyTurns[turnIndex].providerNativeTurnId]
462
+ : []),
463
+ fallbackEventId: fallbackEventIds[firstEntryIndex],
464
+ });
218
465
  continue;
219
- anchorByUserPosition.set(userPosition, matchedTimingIndex);
220
- assignedTimingIndexes.add(matchedTimingIndex);
221
- timingUpperBound = matchedTimingIndex - 1;
222
- }
223
- const assignments = new Map();
224
- const anchors = [...anchorByUserPosition.entries()]
225
- .map(([userPosition, timingIndex]) => ({ userPosition, timingIndex }))
226
- .sort((left, right) => left.userPosition - right.userPosition);
227
- for (const anchor of anchors)
228
- assignments.set(users[anchor.userPosition].entryIndex, turnTimings[anchor.timingIndex]);
229
- const boundaries = [
230
- { userPosition: -1, timingIndex: -1 },
231
- ...anchors,
232
- { userPosition: users.length, timingIndex: turnTimings.length },
233
- ];
234
- for (let boundaryIndex = 0; boundaryIndex < boundaries.length - 1; boundaryIndex += 1) {
235
- const previous = boundaries[boundaryIndex];
236
- const next = boundaries[boundaryIndex + 1];
237
- let timingCursor = next.timingIndex - 1;
238
- for (let userPosition = next.userPosition - 1; userPosition > previous.userPosition; userPosition -= 1) {
239
- const user = users[userPosition];
240
- if (assignments.has(user.entryIndex) || !user.prompt)
466
+ }
467
+ group.assistantTurnIndexes.push(turnIndex);
468
+ group.entryIndexes.push(...historyTurns[turnIndex].entryIndexes);
469
+ const providerNativeTurnId = historyTurns[turnIndex].providerNativeTurnId;
470
+ if (providerNativeTurnId)
471
+ group.providerNativeTurnIds.add(providerNativeTurnId);
472
+ }
473
+ return groups;
474
+ }
475
+ function historyGroupFallbackEventIds(entries) {
476
+ const positionCounts = new Map();
477
+ const sequenceCounts = new Map();
478
+ for (const entry of entries) {
479
+ if (entry.historyPosition)
480
+ positionCounts.set(entry.historyPosition, (positionCounts.get(entry.historyPosition) ?? 0) + 1);
481
+ if (entry.sequence !== undefined)
482
+ sequenceCounts.set(entry.sequence, (sequenceCounts.get(entry.sequence) ?? 0) + 1);
483
+ }
484
+ return entries.map((entry, entryIndex) => {
485
+ if (entry.historyPosition && positionCounts.get(entry.historyPosition) === 1) {
486
+ return `native-history-group:${entry.historyPosition}`;
487
+ }
488
+ const scopePrefix = entry.historyScopeId ? `${entry.historyScopeId}:` : "";
489
+ if (entry.sequence !== undefined && sequenceCounts.get(entry.sequence) === 1) {
490
+ return `native-history-group:${scopePrefix}position:${entry.sequence}`;
491
+ }
492
+ return `native-history-group:${scopePrefix}position:${entryIndex}`;
493
+ });
494
+ }
495
+ function groupNeedsStructuralFallback(entries, group, repeatedProviderTurnIds) {
496
+ return group.userPosition === undefined
497
+ || group.entryIndexes.some((entryIndex) => {
498
+ const entry = entries[entryIndex];
499
+ return entry?.type === "message"
500
+ && ((entry.turnId !== undefined && repeatedProviderTurnIds.has(entry.turnId))
501
+ || (entry.nativeTurnId !== undefined && repeatedProviderTurnIds.has(entry.nativeTurnId)));
502
+ })
503
+ || (group.assistantTurnIndexes.length > 0 && group.entryIndexes.every((entryIndex) => {
504
+ const entry = entries[entryIndex];
505
+ return entry?.type !== "message" || !entry.turnId;
506
+ }));
507
+ }
508
+ function repeatedProviderTurnIdsAcrossGroups(entries, groups) {
509
+ const counts = new Map();
510
+ for (const group of groups) {
511
+ const identities = new Set();
512
+ for (const entryIndex of group.entryIndexes) {
513
+ const entry = entries[entryIndex];
514
+ if (entry?.type !== "message")
241
515
  continue;
242
- for (let timingIndex = timingCursor; timingIndex > previous.timingIndex; timingIndex -= 1) {
243
- if (assignedTimingIndexes.has(timingIndex))
244
- continue;
245
- const timing = turnTimings[timingIndex];
246
- if (normalizedPrompt(timing.userText) !== user.prompt)
247
- continue;
248
- assignments.set(user.entryIndex, timing);
249
- assignedTimingIndexes.add(timingIndex);
250
- timingCursor = timingIndex - 1;
251
- break;
252
- }
516
+ if (entry.turnId)
517
+ identities.add(entry.turnId);
518
+ if (entry.nativeTurnId)
519
+ identities.add(entry.nativeTurnId);
520
+ }
521
+ for (const identity of identities)
522
+ counts.set(identity, (counts.get(identity) ?? 0) + 1);
523
+ }
524
+ return new Set([...counts].filter(([, count]) => count > 1).map(([identity]) => identity));
525
+ }
526
+ function conflictingHistoryGroupIndexes(groups, matches, timingByEventId, invalidUserPositions) {
527
+ const conflictingGroupIndexes = new Set();
528
+ const directClaimants = new Map();
529
+ const outputClaimants = new Map();
530
+ for (const group of groups) {
531
+ if (group.userPosition === undefined
532
+ || (invalidUserPositions.has(group.userPosition) && group.assistantTurnIndexes.length === 0))
533
+ continue;
534
+ const match = matches[group.userPosition];
535
+ if (!match)
536
+ continue;
537
+ const timingClaimants = directClaimants.get(match.timingIndex) ?? [];
538
+ timingClaimants.push(group);
539
+ directClaimants.set(match.timingIndex, timingClaimants);
540
+ if (group.assistantTurnIndexes.length === 0)
541
+ continue;
542
+ const outputTiming = outputTimingForUserTiming(match.timing, timingByEventId);
543
+ if (!outputTiming)
544
+ continue;
545
+ const eventClaimants = outputClaimants.get(outputTiming.eventId) ?? [];
546
+ eventClaimants.push(group);
547
+ outputClaimants.set(outputTiming.eventId, eventClaimants);
548
+ }
549
+ for (const claimants of directClaimants.values()) {
550
+ if (claimants.length <= 1)
551
+ continue;
552
+ for (const claimant of claimants)
553
+ conflictingGroupIndexes.add(claimant.index);
554
+ }
555
+ for (const [eventId, claimants] of outputClaimants) {
556
+ if (claimants.length <= 1 || isLegitimateSteeringCohort(eventId, claimants, matches))
557
+ continue;
558
+ for (const claimant of claimants)
559
+ conflictingGroupIndexes.add(claimant.index);
560
+ }
561
+ return conflictingGroupIndexes;
562
+ }
563
+ function isLegitimateSteeringCohort(outputEventId, groups, matches) {
564
+ const [providerNativeTurnId] = groups[0]?.providerNativeTurnIds ?? [];
565
+ if (!providerNativeTurnId || groups.some((group) => group.providerNativeTurnIds.size !== 1 || !group.providerNativeTurnIds.has(providerNativeTurnId)))
566
+ return false;
567
+ let baseClaimants = 0;
568
+ let steeringClaimants = 0;
569
+ for (const group of groups) {
570
+ const timing = group.userPosition === undefined ? undefined : matches[group.userPosition]?.timing;
571
+ if (!timing)
572
+ return false;
573
+ if (timing.userMessageType === "message_steered" && timing.activeEventId === outputEventId) {
574
+ steeringClaimants += 1;
575
+ }
576
+ else if (timing.eventId === outputEventId) {
577
+ baseClaimants += 1;
578
+ }
579
+ else {
580
+ return false;
253
581
  }
254
582
  }
255
- return assignments;
583
+ return steeringClaimants > 0 && baseClaimants <= 1;
584
+ }
585
+ function outputTimingForUserTiming(userTiming, timingByEventId) {
586
+ if (userTiming.userMessageType !== "message_steered")
587
+ return userTiming;
588
+ return userTiming.activeEventId ? timingByEventId.get(userTiming.activeEventId) : undefined;
589
+ }
590
+ function findConfidentTimingMatch(target, turnTimings) {
591
+ for (const identity of [target.turnId, target.entryId]) {
592
+ if (!identity)
593
+ continue;
594
+ const timingIndex = turnTimings.findIndex((timing) => timing.eventId === identity);
595
+ if (timingIndex >= 0)
596
+ return { timing: turnTimings[timingIndex], timingIndex, confidence: "identity" };
597
+ }
598
+ if (!target.prompt)
599
+ return undefined;
600
+ const candidates = turnTimings.flatMap((timing, timingIndex) => normalizedPrompt(timing.userText) === target.prompt ? [{ timing, timingIndex }] : []);
601
+ if (!candidates.length)
602
+ return undefined;
603
+ const timestampEvidence = candidates.flatMap((candidate) => {
604
+ const startedAt = parsedTimestamp(candidate.timing.startedAt);
605
+ const completedAt = parsedTimestamp(candidate.timing.completedAt);
606
+ const startDistance = startedAt === undefined || target.userAt === undefined
607
+ ? undefined
608
+ : Math.abs(startedAt - target.userAt);
609
+ const completionDistance = completedAt === undefined || target.assistantAt === undefined
610
+ ? undefined
611
+ : Math.abs(completedAt - target.assistantAt);
612
+ return [
613
+ ...(startDistance !== undefined && startDistance <= MAX_CONFIDENT_TIMESTAMP_DISTANCE_MS
614
+ ? [{ ...candidate, endpoint: "start", distance: startDistance }]
615
+ : []),
616
+ ...(completionDistance !== undefined && completionDistance <= MAX_CONFIDENT_TIMESTAMP_DISTANCE_MS
617
+ ? [{ ...candidate, endpoint: "completion", distance: completionDistance }]
618
+ : []),
619
+ ];
620
+ }).sort((left, right) => left.distance - right.distance || left.timingIndex - right.timingIndex);
621
+ const best = timestampEvidence[0];
622
+ if (!best)
623
+ return undefined;
624
+ const bestTimingIndices = new Set(timestampEvidence
625
+ .filter((evidence) => evidence.distance === best.distance)
626
+ .map((evidence) => evidence.timingIndex));
627
+ return bestTimingIndices.size === 1
628
+ ? { timing: best.timing, timingIndex: best.timingIndex, confidence: "timestamp" }
629
+ : undefined;
256
630
  }
257
631
  function collectAssistantTurn(entries, startIndex) {
258
632
  const turnEntries = [];
@@ -268,7 +642,7 @@ function collectAssistantTurn(entries, startIndex) {
268
642
  }
269
643
  return { entries: turnEntries, nextIndex: index };
270
644
  }
271
- function createUserMessageNode(piboSessionId, entry, entryIndex, timing) {
645
+ function createUserMessageNode(piboSessionId, entry, entryIndex, timing, fallbackEventId) {
272
646
  const text = historyMessageText(entry);
273
647
  const notification = parseRunNotificationText(text);
274
648
  const source = traceSource(entry.source);
@@ -286,13 +660,17 @@ function createUserMessageNode(piboSessionId, entry, entryIndex, timing) {
286
660
  });
287
661
  }
288
662
  const userMessageType = timing?.userMessageType ?? "message_queued";
289
- const eventId = entry.turnId ?? timing?.eventId;
663
+ const eventId = canonicalProductTurnId(entry, timing, fallbackEventId);
290
664
  const eventIdentity = eventId ? `event:${userMessageType}:${eventId}` : undefined;
291
665
  return {
292
666
  id: eventIdentity ?? historyEntryNodeId(entry),
293
667
  entryId: entry.nativeEntryId,
668
+ nativeTurnId: nativeHistoryTurnId(entry),
294
669
  piboSessionId,
295
670
  eventId,
671
+ parentId: timing?.userMessageType === "message_steered" && timing.activeEventId
672
+ ? messageTurnNodeId(timing.activeEventId)
673
+ : undefined,
296
674
  type: "user.message",
297
675
  title: "User Message",
298
676
  status: entry.status === "running" ? "running" : entry.status === "error" ? "error" : "done",
@@ -306,17 +684,20 @@ function createUserMessageNode(piboSessionId, entry, entryIndex, timing) {
306
684
  children: [],
307
685
  };
308
686
  }
309
- function createAssistantTurnNodes(piboSessionId, entries, timing) {
687
+ function createAssistantTurnNodes(piboSessionId, entries, timing, projectionState = emptyAssistantTurnProjectionState(), fallbackEventId, qualifiedNativeToolCallIds = new Set()) {
310
688
  const firstAssistant = entries.find(({ entry }) => entry.role === "assistant");
311
- if (!firstAssistant)
689
+ const identityEntry = firstAssistant ?? entries[0];
690
+ if (!identityEntry)
312
691
  return [];
692
+ const eventId = canonicalProductTurnId(identityEntry.entry, timing, fallbackEventId);
313
693
  const orderedNodes = [];
314
694
  const toolsByCallId = new Map();
315
- let assistantPartOrdinal = 0;
316
- let reasoningPartOrdinal = 0;
317
695
  for (const { entry, index: entryIndex } of entries) {
318
696
  if (entry.role === "tool") {
319
- mergePersistedToolResult(toolsByCallId, orderedNodes, entry, piboSessionId, entryIndex);
697
+ const toolInvocationOrdinal = entry.toolCallId
698
+ ? consumeToolCallOrdinal(projectionState, entry.toolCallId)
699
+ : 0;
700
+ mergePersistedToolResult(toolsByCallId, orderedNodes, entry, piboSessionId, entryIndex, eventId, toolInvocationOrdinal, qualifiedNativeToolCallIds.has(entry.toolCallId ?? ""));
320
701
  continue;
321
702
  }
322
703
  const responseStatus = historyMessageStatus(entry);
@@ -324,28 +705,31 @@ function createAssistantTurnNodes(piboSessionId, entries, timing) {
324
705
  const parts = historyMessageParts(entry);
325
706
  for (const [contentPartIndex, part] of parts.entries()) {
326
707
  if (part.type === "reasoning" && hasVisibleText(part.text)) {
327
- const thinkingIndex = timing?.reasoningIndices?.[reasoningPartOrdinal] ?? reasoningPartOrdinal;
708
+ const thinkingIndex = timing?.reasoningIndices?.[projectionState.reasoningPartOrdinal]
709
+ ?? projectionState.reasoningPartOrdinal;
328
710
  orderedNodes.push(createReasoningNode({
329
711
  piboSessionId,
330
712
  entry,
331
713
  entryIndex,
332
714
  contentPartIndex,
333
715
  thinkingIndex,
334
- eventId: entry.turnId ?? timing?.eventId,
716
+ eventId,
335
717
  thinking: part.text,
336
718
  }));
337
- reasoningPartOrdinal += 1;
719
+ projectionState.reasoningPartOrdinal += 1;
338
720
  }
339
721
  else if (part.type === "text" && part.text !== "") {
340
722
  if (!responseNode) {
341
- const assistantIndex = entry.assistantIndex ?? timing?.assistantIndices?.[assistantPartOrdinal] ?? assistantPartOrdinal;
723
+ const assistantIndex = timing?.assistantIndices?.[projectionState.assistantPartOrdinal]
724
+ ?? entry.assistantIndex
725
+ ?? projectionState.assistantPartOrdinal;
342
726
  responseNode = createAssistantMessageNode({
343
727
  piboSessionId,
344
728
  entry,
345
729
  entryIndex,
346
730
  contentPartIndex,
347
731
  assistantIndex,
348
- eventId: entry.turnId ?? timing?.eventId,
732
+ eventId,
349
733
  status: responseStatus,
350
734
  text: part.text,
351
735
  error: entry.error,
@@ -360,15 +744,18 @@ function createAssistantTurnNodes(piboSessionId, entries, timing) {
360
744
  }
361
745
  }
362
746
  else if (part.type === "tool_call") {
363
- const toolNode = createToolCallNode(piboSessionId, entry, entryIndex, contentPartIndex, part);
747
+ const toolInvocationOrdinal = reserveToolCallOrdinal(projectionState, part.toolCallId);
748
+ const toolNode = createToolCallNode(piboSessionId, entry, entryIndex, contentPartIndex, part, eventId, toolInvocationOrdinal, qualifiedNativeToolCallIds.has(part.toolCallId));
364
749
  orderedNodes.push(toolNode);
365
- toolsByCallId.set(part.toolCallId, toolNode);
750
+ const pendingTools = toolsByCallId.get(part.toolCallId) ?? [];
751
+ pendingTools.push(toolNode);
752
+ toolsByCallId.set(part.toolCallId, pendingTools);
366
753
  }
367
754
  }
368
755
  if (responseNode) {
369
756
  responseNode.status = responseStatus;
370
757
  responseNode.error = entry.error;
371
- assistantPartOrdinal += 1;
758
+ projectionState.assistantPartOrdinal += 1;
372
759
  }
373
760
  }
374
761
  const finalNode = orderedNodes.at(-1);
@@ -384,9 +771,10 @@ function createReasoningNode(input) {
384
771
  return {
385
772
  id: eventIdentity ? thinkingNodeId(eventIdentity) : `${historyEntryNodeId(input.entry)}:thinking:${input.contentPartIndex}`,
386
773
  entryId: input.entry.nativeEntryId,
774
+ nativeTurnId: nativeHistoryTurnId(input.entry),
387
775
  piboSessionId: input.piboSessionId,
388
776
  eventId: input.eventId,
389
- parentId: input.entry.source === "product" && input.eventId ? messageTurnNodeId(input.eventId) : undefined,
777
+ parentId: historyTurnParentId(input.entry, input.eventId),
390
778
  type: "model.reasoning",
391
779
  title: "Thinking",
392
780
  status: "done",
@@ -399,14 +787,16 @@ function createReasoningNode(input) {
399
787
  children: [],
400
788
  };
401
789
  }
402
- function createToolCallNode(piboSessionId, entry, entryIndex, contentPartIndex, part) {
790
+ function createToolCallNode(piboSessionId, entry, entryIndex, contentPartIndex, part, eventId, toolInvocationOrdinal = 0, qualifyProjectionIdentity = false) {
403
791
  const source = traceSource(entry.source);
792
+ const toolNodeId = historyToolNodeId(entry, part.toolCallId, eventId, toolInvocationOrdinal, qualifyProjectionIdentity);
404
793
  return {
405
- id: `tool:${part.toolCallId}`,
794
+ id: toolNodeId,
406
795
  entryId: entry.nativeEntryId,
796
+ nativeTurnId: nativeHistoryTurnId(entry),
407
797
  piboSessionId,
408
- eventId: entry.turnId,
409
- parentId: entry.source === "product" && entry.turnId ? messageTurnNodeId(entry.turnId) : undefined,
798
+ eventId,
799
+ parentId: historyTurnParentId(entry, eventId),
410
800
  toolCallId: part.toolCallId,
411
801
  type: isSubagentToolName(part.toolName) ? "agent.delegation" : "tool.call",
412
802
  title: part.toolName,
@@ -414,41 +804,48 @@ function createToolCallNode(piboSessionId, entry, entryIndex, contentPartIndex,
414
804
  startedAt: entry.createdAt,
415
805
  input: part.input ?? {},
416
806
  source,
417
- stableKey: `tool:${part.toolCallId}`,
807
+ stableKey: toolNodeId,
418
808
  orderKey: historyTraceOrder(historyIndex(entry, entryIndex), contentPartIndex, isSubagentToolName(part.toolName) ? "agent.delegation" : "tool.call", source),
419
809
  children: [],
420
810
  };
421
811
  }
422
- function mergePersistedToolResult(toolsByCallId, childNodes, entry, piboSessionId, entryIndex) {
812
+ function mergePersistedToolResult(toolsByCallId, childNodes, entry, piboSessionId, entryIndex, eventId, toolInvocationOrdinal = 0, qualifyProjectionIdentity = false) {
423
813
  const toolCallId = entry.toolCallId;
424
814
  if (!toolCallId)
425
815
  return;
426
- let toolNode = toolsByCallId.get(toolCallId);
816
+ const pendingTools = toolsByCallId.get(toolCallId);
817
+ let toolNode = pendingTools?.shift();
818
+ if (pendingTools?.length === 0)
819
+ toolsByCallId.delete(toolCallId);
427
820
  if (!toolNode) {
428
- toolNode = createMissingToolResultNode(piboSessionId, entry, entryIndex, toolCallId);
821
+ toolNode = createMissingToolResultNode(piboSessionId, entry, entryIndex, toolCallId, eventId, toolInvocationOrdinal, qualifyProjectionIdentity);
429
822
  childNodes.push(toolNode);
430
- toolsByCallId.set(toolCallId, toolNode);
431
823
  }
824
+ toolNode.eventId ??= eventId;
825
+ toolNode.nativeTurnId ??= nativeHistoryTurnId(entry);
432
826
  toolNode.status = entry.isError === true || entry.status === "error" ? "error" : "done";
433
827
  toolNode.completedAt = entry.createdAt;
434
828
  toolNode.output = entry.result ?? { content: entry.content };
435
829
  toolNode.error = toolNode.status === "error" ? stringifyPreview(toolNode.output) : undefined;
436
830
  attachAsyncAgentRunNode(toolNode, piboSessionId, entry.createdAt);
437
831
  }
438
- function createMissingToolResultNode(piboSessionId, entry, entryIndex, toolCallId) {
832
+ function createMissingToolResultNode(piboSessionId, entry, entryIndex, toolCallId, eventId, toolInvocationOrdinal = 0, qualifyProjectionIdentity = false) {
439
833
  const source = traceSource(entry.source);
834
+ const toolNodeId = historyToolNodeId(entry, toolCallId, eventId, toolInvocationOrdinal, qualifyProjectionIdentity);
440
835
  return {
441
- id: `tool:${toolCallId}`,
836
+ id: toolNodeId,
442
837
  entryId: entry.nativeEntryId,
838
+ nativeTurnId: nativeHistoryTurnId(entry),
443
839
  piboSessionId,
444
- eventId: entry.turnId,
840
+ eventId,
841
+ parentId: historyTurnParentId(entry, eventId),
445
842
  toolCallId,
446
843
  type: "tool.result",
447
844
  title: entry.toolName ?? "Tool Result",
448
845
  status: "done",
449
846
  startedAt: entry.createdAt,
450
847
  source,
451
- stableKey: `tool:${toolCallId}`,
848
+ stableKey: toolNodeId,
452
849
  orderKey: historyTraceOrder(historyIndex(entry, entryIndex), 0, "tool.result", source),
453
850
  children: [],
454
851
  };
@@ -459,9 +856,10 @@ function createAssistantMessageNode(input) {
459
856
  return {
460
857
  id: eventIdentity ? assistantMessageNodeId(eventIdentity) : `${historyEntryNodeId(input.entry)}:response`,
461
858
  entryId: input.entry.nativeEntryId,
859
+ nativeTurnId: nativeHistoryTurnId(input.entry),
462
860
  piboSessionId: input.piboSessionId,
463
861
  eventId: input.eventId,
464
- parentId: input.entry.source === "product" && input.eventId ? messageTurnNodeId(input.eventId) : undefined,
862
+ parentId: historyTurnParentId(input.entry, input.eventId),
465
863
  type: "assistant.message",
466
864
  title: "Agent Message",
467
865
  status: input.status,
@@ -491,12 +889,80 @@ function historyMessageStatus(entry) {
491
889
  return "running";
492
890
  return "done";
493
891
  }
494
- function isSettledAssistantEntry(entry) {
495
- return entry.status !== "running" && entry.metadata?.stopReason !== "toolUse" && entry.metadata?.stopReason !== "tool_use";
892
+ function canonicalProductTurnId(entry, timing, fallbackEventId) {
893
+ return fallbackEventId ?? timing?.eventId ?? entry.turnId;
894
+ }
895
+ function nativeHistoryTurnId(entry) {
896
+ return entry.nativeTurnId ?? (entry.source === "native" ? entry.turnId : undefined);
897
+ }
898
+ function historyTurnParentId(entry, eventId) {
899
+ return entry.source === "product" && eventId ? messageTurnNodeId(eventId) : undefined;
496
900
  }
497
901
  function historyEntryNodeId(entry) {
902
+ if (entry.source === "native" && entry.historyPosition)
903
+ return `history:${entry.historyPosition}`;
498
904
  return entry.source === "native" && entry.nativeEntryId ? `entry:${entry.nativeEntryId}` : `history:${entry.id}`;
499
905
  }
906
+ function historyToolNodeId(entry, toolCallId, eventId, toolInvocationOrdinal, qualifyProjectionIdentity) {
907
+ if (entry.source !== "native" || !qualifyProjectionIdentity)
908
+ return `tool:${toolCallId}`;
909
+ return qualifiedHistoryToolNodeId(toolCallId, eventId ?? `native-history-tool:${entry.historyPosition ?? entry.id}`, toolInvocationOrdinal);
910
+ }
911
+ function nativeToolCallIdsRequiringQualification(entries, reconciliationProof) {
912
+ const localNativeToolCallIds = nativeToolCallIds(entries);
913
+ if (!reconciliationProof)
914
+ return duplicateNativeToolCallIds(entries);
915
+ if (!reconciliationProof.complete
916
+ || reconciliationProof.entries.length > TRACE_RECONCILIATION_ENTRY_CAP
917
+ || !structuralClaimMatchesPage(entries, reconciliationProof))
918
+ return localNativeToolCallIds;
919
+ return duplicateNativeToolCallIds(reconciliationProof.entries);
920
+ }
921
+ function nativeToolCallIds(entries) {
922
+ const result = new Set();
923
+ for (const entry of entries) {
924
+ if (entry.type !== "message" || entry.source !== "native")
925
+ continue;
926
+ if (entry.role === "assistant") {
927
+ for (const part of historyMessageParts(entry)) {
928
+ if (part.type === "tool_call")
929
+ result.add(part.toolCallId);
930
+ }
931
+ }
932
+ else if (entry.role === "tool" && entry.toolCallId) {
933
+ result.add(entry.toolCallId);
934
+ }
935
+ }
936
+ return result;
937
+ }
938
+ function duplicateNativeToolCallIds(entries) {
939
+ const counts = new Map();
940
+ for (let index = 0; index < entries.length; index += 1) {
941
+ const entry = entries[index];
942
+ if (entry.type !== "message" || (entry.role !== "assistant" && entry.role !== "tool"))
943
+ continue;
944
+ const turn = collectAssistantTurn(entries, index);
945
+ const callIds = new Set();
946
+ for (const { entry: turnEntry } of turn.entries) {
947
+ if (turnEntry.source !== "native")
948
+ continue;
949
+ if (turnEntry.role === "assistant") {
950
+ for (const part of historyMessageParts(turnEntry)) {
951
+ if (part.type !== "tool_call")
952
+ continue;
953
+ callIds.add(part.toolCallId);
954
+ counts.set(part.toolCallId, (counts.get(part.toolCallId) ?? 0) + 1);
955
+ }
956
+ }
957
+ else if (turnEntry.toolCallId && !callIds.has(turnEntry.toolCallId)) {
958
+ callIds.add(turnEntry.toolCallId);
959
+ counts.set(turnEntry.toolCallId, (counts.get(turnEntry.toolCallId) ?? 0) + 1);
960
+ }
961
+ }
962
+ index = turn.nextIndex - 1;
963
+ }
964
+ return new Set([...counts].filter(([, count]) => count > 1).map(([toolCallId]) => toolCallId));
965
+ }
500
966
  function historyEntryStableKey(entry) {
501
967
  return historyEntryNodeId(entry);
502
968
  }
@@ -511,9 +977,43 @@ function normalizedPrompt(value) {
511
977
  return normalized || undefined;
512
978
  }
513
979
  function parsedTimestamp(value) {
514
- if (!value)
980
+ if (typeof value !== "string")
981
+ return undefined;
982
+ const match = PERSISTED_TIMESTAMP_PATTERN.exec(value);
983
+ if (!match)
984
+ return undefined;
985
+ const year = Number(match[1]);
986
+ const month = Number(match[2]);
987
+ const day = Number(match[3]);
988
+ const hour = Number(match[4]);
989
+ const minute = Number(match[5]);
990
+ const second = Number(match[6]);
991
+ const millisecond = Number((match[7] ?? "").padEnd(3, "0").slice(0, 3));
992
+ const offsetHour = Number(match[10] ?? 0);
993
+ const offsetMinute = Number(match[11] ?? 0);
994
+ if (month < 1 || month > 12 ||
995
+ day < 1 || day > 31 ||
996
+ hour > 23 ||
997
+ minute > 59 ||
998
+ second > 59 ||
999
+ offsetHour > 23 ||
1000
+ offsetMinute > 59)
1001
+ return undefined;
1002
+ const local = new Date(0);
1003
+ local.setUTCFullYear(year, month - 1, day);
1004
+ local.setUTCHours(hour, minute, second, millisecond);
1005
+ if (local.getUTCFullYear() !== year ||
1006
+ local.getUTCMonth() !== month - 1 ||
1007
+ local.getUTCDate() !== day ||
1008
+ local.getUTCHours() !== hour ||
1009
+ local.getUTCMinutes() !== minute ||
1010
+ local.getUTCSeconds() !== second)
515
1011
  return undefined;
516
- const timestamp = new Date(value).getTime();
1012
+ const offsetSign = match[9] === "-" ? -1 : 1;
1013
+ const offsetMs = match[8] === "Z"
1014
+ ? 0
1015
+ : offsetSign * (offsetHour * 60 + offsetMinute) * 60_000;
1016
+ const timestamp = local.getTime() - offsetMs;
517
1017
  return Number.isFinite(timestamp) ? timestamp : undefined;
518
1018
  }
519
1019
  function hasVisibleText(value) {