@pasko70/pibo 1.11.4 → 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.
Files changed (60) hide show
  1. package/dist/apps/chat/chat-trace-helpers.js +59 -2
  2. package/dist/apps/chat/data/timeline-query-service.js +1 -1
  3. package/dist/apps/chat/web-app.js +16 -8
  4. package/dist/apps/chat-ui/assets/{dist-BXjLdcn6.js → dist-3bD-92Wo.js} +1 -1
  5. package/dist/apps/{context-files-ui/assets/dist-BQK_wUMR.js → chat-ui/assets/dist-BFjy5Y59.js} +1 -1
  6. package/dist/apps/{context-files-ui/assets/dist-Dtv5S2qd.js → chat-ui/assets/dist-BKwQlbhS.js} +1 -1
  7. package/dist/apps/{context-files-ui/assets/dist-DT9QkgTN.js → chat-ui/assets/dist-Cp68y_h5.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-D9B41N2f.js → dist-DE0y_EI7.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-TS4EA6YQ.js → dist-DPb0yDi4.js} +1 -1
  10. package/dist/apps/{context-files-ui/assets/dist-B07Whoa6.js → chat-ui/assets/dist-D_dQ5GaA.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-yjwDcRbw.js → dist-Daq2KRpq.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-CjXSEbsr.js → dist-aZnuCezL.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-CSBDio7C.js → dist-q6Vpm5yH.js} +1 -1
  14. package/dist/apps/chat-ui/assets/index-BJoo_GJP.css +1 -0
  15. package/dist/apps/chat-ui/assets/index-DXPKuGfs.js +237 -0
  16. package/dist/apps/chat-ui/index.html +2 -2
  17. package/dist/apps/chat-vscode-web/assets/index-B3dSrp-L.js +41 -0
  18. package/dist/apps/chat-vscode-web/index.html +1 -1
  19. package/dist/apps/{chat-ui/assets/dist-BeKdUsyg.js → context-files-ui/assets/dist-BYcsFgKp.js} +1 -1
  20. package/dist/apps/context-files-ui/assets/{dist-TG9QLHKj.js → dist-Bdudyq9I.js} +1 -1
  21. package/dist/apps/context-files-ui/assets/{dist-BFlEjppv.js → dist-BvIW3V94.js} +1 -1
  22. package/dist/apps/context-files-ui/assets/{dist-De6eI3Cr.js → dist-CSLUWBzz.js} +1 -1
  23. package/dist/apps/{chat-ui/assets/dist-CNv02m3N.js → context-files-ui/assets/dist-CT-91DFe.js} +1 -1
  24. package/dist/apps/{chat-ui/assets/dist-BtQ20SMU.js → context-files-ui/assets/dist-DcsDivye.js} +1 -1
  25. package/dist/apps/context-files-ui/assets/{dist-BACxmdQ2.js → dist-DeozI4lc.js} +1 -1
  26. package/dist/apps/context-files-ui/assets/{dist-D68eIqmX.js → dist-f2E1-leP.js} +1 -1
  27. package/dist/apps/context-files-ui/assets/{dist-CigbyRK5.js → dist-sus90AFR.js} +1 -1
  28. package/dist/apps/{chat-ui/assets/dist-TAhl1jC0.js → context-files-ui/assets/dist-uFphVGi8.js} +1 -1
  29. package/dist/apps/context-files-ui/assets/index-CQ25ablG.js +202 -0
  30. package/dist/apps/context-files-ui/index.html +1 -1
  31. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  32. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.12.1.vsix +0 -0
  33. package/dist/core/gateway-resource-guard.js +46 -15
  34. package/dist/core/routed-session.js +40 -1
  35. package/dist/core/runtime.js +2 -0
  36. package/dist/core/session-errors.js +3 -0
  37. package/dist/core/session-router.js +25 -2
  38. package/dist/core/transcript-integrity.js +431 -0
  39. package/dist/data/pibo-store.js +2 -0
  40. package/dist/data/telemetry.js +148 -0
  41. package/dist/debug/index.js +11 -0
  42. package/dist/debug/web.js +1 -1
  43. package/dist/gateway/server.js +1 -0
  44. package/dist/reliability/store.js +11 -6
  45. package/dist/runs/registry.js +38 -1
  46. package/dist/runs/resource-isolation.js +437 -0
  47. package/dist/runs/tools.js +6 -3
  48. package/dist/sessions/pibo-data-store.js +114 -0
  49. package/dist/shared/trace-engine.js +3 -3
  50. package/dist/shared/trace-event-projection.js +165 -36
  51. package/dist/shared/trace-nodes.js +6 -4
  52. package/dist/shared/trace-page-merge.js +106 -2
  53. package/dist/shared/trace-transcript.js +194 -36
  54. package/dist/tools/cdp-client.js +87 -3
  55. package/package.json +1 -1
  56. package/dist/apps/chat-ui/assets/index-BoRl0OB_.js +0 -473
  57. package/dist/apps/chat-ui/assets/index-aTOOomMF.css +0 -1
  58. package/dist/apps/chat-vscode-web/assets/index-DGcSd2ZH.js +0 -275
  59. package/dist/apps/context-files-ui/assets/index-B5j33Q2B.js +0 -438
  60. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.11.4.vsix +0 -0
@@ -3,29 +3,37 @@ import { attachAsyncAgentRunNode, reconcileAsyncAgentRunStatuses } from "./trace
3
3
  import { sortTraceNodes } from "./trace-nodes.js";
4
4
  import { createRunNotificationNode, parseRunNotificationText } from "./trace-run-notifications.js";
5
5
  import { isSubagentToolName } from "./trace-subagent-links.js";
6
- export function projectTranscriptEntries(entries, sessionStatus, openTranscriptEventIds) {
6
+ import { assistantMessageNodeId, thinkingNodeId } from "./trace-event-projection.js";
7
+ export function projectTranscriptEntries(entries, sessionStatus, openTranscriptEventIds, turnTimings = []) {
7
8
  if (sessionStatus !== "running" || openTranscriptEventIds.size === 0)
8
9
  return entries;
10
+ const userTimingAssignments = assignTranscriptUserTimings(entries, turnTimings);
9
11
  let lastUserMessageIndex = -1;
10
- for (let index = entries.length - 1; index >= 0; index -= 1) {
12
+ let lastUserEventId;
13
+ for (let index = 0; index < entries.length; index += 1) {
11
14
  const entry = entries[index];
12
- if (entry.type === "message" && messageRole(entry) === "user") {
13
- lastUserMessageIndex = index;
14
- break;
15
- }
15
+ if (entry.type !== "message" || messageRole(entry) !== "user")
16
+ continue;
17
+ lastUserMessageIndex = index;
18
+ lastUserEventId = openTranscriptEventIds.has(entry.id)
19
+ ? entry.id
20
+ : userTimingAssignments.get(index)?.eventId;
16
21
  }
17
- return lastUserMessageIndex === -1 ? entries : entries.slice(0, lastUserMessageIndex);
22
+ return lastUserMessageIndex !== -1 && lastUserEventId && openTranscriptEventIds.has(lastUserEventId)
23
+ ? entries.slice(0, lastUserMessageIndex)
24
+ : entries;
18
25
  }
19
26
  export function traceNodesFromEntries(piboSessionId, entries, turnTimings = []) {
20
27
  const nodes = [];
21
28
  const turnTimingAssignments = assignTranscriptTurnTimings(entries, turnTimings);
29
+ const userTimingAssignments = assignTranscriptUserTimings(entries, turnTimings);
22
30
  let assistantTurnIndex = 0;
23
31
  for (let index = 0; index < entries.length; index += 1) {
24
32
  const entry = entries[index];
25
33
  if (entry.type === "message") {
26
34
  const role = messageRole(entry);
27
35
  if (role === "user") {
28
- nodes.push(createUserMessageNode(piboSessionId, entry, messageContent(entry), index));
36
+ nodes.push(createUserMessageNode(piboSessionId, entry, messageContent(entry), index, userTimingAssignments.get(index)));
29
37
  }
30
38
  else if (role === "assistant" || role === "toolResult") {
31
39
  const turn = collectAssistantTurn(entries, index);
@@ -70,9 +78,10 @@ function messageParts(entry) {
70
78
  return [{ type: "text", text: content }];
71
79
  return Array.isArray(content) ? content : [];
72
80
  }
73
- function assignTranscriptTurnTimings(entries, turnTimings) {
74
- const transcriptTurns = [];
81
+ function collectTranscriptTurnTimingTargets(entries) {
82
+ const targets = [];
75
83
  let latestUserText;
84
+ let latestUserEntryIndex;
76
85
  for (let index = 0; index < entries.length; index += 1) {
77
86
  const entry = entries[index];
78
87
  if (entry.type !== "message")
@@ -80,6 +89,7 @@ function assignTranscriptTurnTimings(entries, turnTimings) {
80
89
  const role = messageRole(entry);
81
90
  if (role === "user") {
82
91
  latestUserText = normalizedPrompt(extractText(messageContent(entry)));
92
+ latestUserEntryIndex = index;
83
93
  continue;
84
94
  }
85
95
  if (role !== "assistant" && role !== "toolResult")
@@ -87,12 +97,22 @@ function assignTranscriptTurnTimings(entries, turnTimings) {
87
97
  const turn = collectAssistantTurn(entries, index);
88
98
  const lastAssistant = [...turn.entries].reverse().find(({ entry: turnEntry }) => messageRole(turnEntry) === "assistant");
89
99
  if (lastAssistant) {
90
- transcriptTurns.push({ prompt: latestUserText, assistantAt: parsedTimestamp(lastAssistant.entry.timestamp) });
100
+ targets.push({
101
+ prompt: latestUserText,
102
+ userEntryIndex: latestUserEntryIndex,
103
+ assistantAt: parsedTimestamp(lastAssistant.entry.timestamp),
104
+ settled: isSettledAssistantEntry(lastAssistant.entry),
105
+ });
91
106
  }
92
107
  index = turn.nextIndex - 1;
93
108
  }
109
+ return targets;
110
+ }
111
+ function assignTranscriptTurnTimings(entries, turnTimings) {
112
+ const messageTurnTimings = turnTimings.filter((timing) => timing.userMessageType !== "message_steered");
113
+ const transcriptTurns = collectTranscriptTurnTimingTargets(entries);
94
114
  const assignments = Array(transcriptTurns.length).fill(undefined);
95
- let timingCursor = turnTimings.length - 1;
115
+ let timingCursor = messageTurnTimings.length - 1;
96
116
  for (let turnIndex = transcriptTurns.length - 1; turnIndex >= 0; turnIndex -= 1) {
97
117
  const transcriptTurn = transcriptTurns[turnIndex];
98
118
  if (!transcriptTurn?.prompt)
@@ -100,7 +120,7 @@ function assignTranscriptTurnTimings(entries, turnTimings) {
100
120
  let matchedIndex;
101
121
  let matchedDistance = Number.POSITIVE_INFINITY;
102
122
  for (let timingIndex = timingCursor; timingIndex >= 0; timingIndex -= 1) {
103
- const timing = turnTimings[timingIndex];
123
+ const timing = messageTurnTimings[timingIndex];
104
124
  if (normalizedPrompt(timing?.userText) !== transcriptTurn.prompt)
105
125
  continue;
106
126
  const completedAt = parsedTimestamp(timing?.completedAt);
@@ -114,11 +134,133 @@ function assignTranscriptTurnTimings(entries, turnTimings) {
114
134
  }
115
135
  if (matchedIndex === undefined)
116
136
  continue;
117
- assignments[turnIndex] = turnTimings[matchedIndex];
137
+ assignments[turnIndex] = messageTurnTimings[matchedIndex];
118
138
  timingCursor = matchedIndex - 1;
119
139
  }
120
140
  return assignments;
121
141
  }
142
+ function assignTranscriptUserTimings(entries, turnTimings) {
143
+ const users = [];
144
+ const userPositionByEntryIndex = new Map();
145
+ for (let entryIndex = 0; entryIndex < entries.length; entryIndex += 1) {
146
+ const entry = entries[entryIndex];
147
+ if (entry.type !== "message" || messageRole(entry) !== "user")
148
+ continue;
149
+ userPositionByEntryIndex.set(entryIndex, users.length);
150
+ users.push({
151
+ entryIndex,
152
+ entryId: entry.id,
153
+ prompt: normalizedPrompt(extractText(messageContent(entry))),
154
+ });
155
+ }
156
+ if (!users.length || !turnTimings.length)
157
+ return new Map();
158
+ const timingIndexByEventId = new Map(turnTimings.map((timing, timingIndex) => [timing.eventId, timingIndex]));
159
+ const anchorByUserPosition = new Map();
160
+ const assignedTimingIndexes = new Set();
161
+ for (let userPosition = 0; userPosition < users.length; userPosition += 1) {
162
+ const timingIndex = timingIndexByEventId.get(users[userPosition].entryId);
163
+ if (timingIndex === undefined || assignedTimingIndexes.has(timingIndex))
164
+ continue;
165
+ anchorByUserPosition.set(userPosition, timingIndex);
166
+ assignedTimingIndexes.add(timingIndex);
167
+ }
168
+ const transcriptTurns = collectTranscriptTurnTimingTargets(entries);
169
+ let timingUpperBound = turnTimings.length - 1;
170
+ for (let turnIndex = transcriptTurns.length - 1; turnIndex >= 0; turnIndex -= 1) {
171
+ const transcriptTurn = transcriptTurns[turnIndex];
172
+ const userPosition = transcriptTurn?.userEntryIndex === undefined
173
+ ? undefined
174
+ : userPositionByEntryIndex.get(transcriptTurn.userEntryIndex);
175
+ if (!transcriptTurn?.prompt || userPosition === undefined)
176
+ continue;
177
+ const exactAnchor = anchorByUserPosition.get(userPosition);
178
+ if (exactAnchor !== undefined) {
179
+ timingUpperBound = Math.min(timingUpperBound, exactAnchor - 1);
180
+ continue;
181
+ }
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) !== transcriptTurn.prompt)
188
+ continue;
189
+ steeringTimingIndex = timingIndex;
190
+ break;
191
+ }
192
+ if (steeringTimingIndex !== undefined) {
193
+ anchorByUserPosition.set(userPosition, steeringTimingIndex);
194
+ assignedTimingIndexes.add(steeringTimingIndex);
195
+ timingUpperBound = steeringTimingIndex - 1;
196
+ continue;
197
+ }
198
+ if (!transcriptTurn.settled)
199
+ 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)
205
+ continue;
206
+ if (normalizedPrompt(timing.userText) !== transcriptTurn.prompt)
207
+ continue;
208
+ const completedAt = parsedTimestamp(timing.completedAt);
209
+ const distance = completedAt === undefined || transcriptTurn.assistantAt === undefined
210
+ ? Number.POSITIVE_INFINITY
211
+ : Math.abs(completedAt - transcriptTurn.assistantAt);
212
+ if (matchedTimingIndex === undefined || distance < matchedDistance) {
213
+ matchedTimingIndex = timingIndex;
214
+ matchedDistance = distance;
215
+ }
216
+ }
217
+ if (matchedTimingIndex === undefined)
218
+ 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
+ }
230
+ const boundaries = [
231
+ { userPosition: -1, timingIndex: -1 },
232
+ ...anchors,
233
+ { userPosition: users.length, timingIndex: turnTimings.length },
234
+ ];
235
+ for (let boundaryIndex = 0; boundaryIndex < boundaries.length - 1; boundaryIndex += 1) {
236
+ const previous = boundaries[boundaryIndex];
237
+ const next = boundaries[boundaryIndex + 1];
238
+ let timingCursor = next.timingIndex - 1;
239
+ for (let userPosition = next.userPosition - 1; userPosition > previous.userPosition; userPosition -= 1) {
240
+ const user = users[userPosition];
241
+ if (assignments.has(user.entryIndex) || !user.prompt)
242
+ continue;
243
+ for (let timingIndex = timingCursor; timingIndex > previous.timingIndex; timingIndex -= 1) {
244
+ if (assignedTimingIndexes.has(timingIndex))
245
+ continue;
246
+ const timing = turnTimings[timingIndex];
247
+ if (normalizedPrompt(timing.userText) !== user.prompt)
248
+ continue;
249
+ assignments.set(user.entryIndex, timing);
250
+ assignedTimingIndexes.add(timingIndex);
251
+ timingCursor = timingIndex - 1;
252
+ break;
253
+ }
254
+ }
255
+ }
256
+ return assignments;
257
+ }
258
+ function isSettledAssistantEntry(entry) {
259
+ const message = entry.message;
260
+ if (message.stopReason === "toolUse" || message.stopReason === "tool_use")
261
+ return false;
262
+ return message.status !== "streaming" && message.status !== "in_progress";
263
+ }
122
264
  function normalizedPrompt(value) {
123
265
  const normalized = value?.replace(/\s+/g, " ").trim();
124
266
  return normalized || undefined;
@@ -144,7 +286,7 @@ function collectAssistantTurn(entries, startIndex) {
144
286
  }
145
287
  return { entries: turnEntries, nextIndex: index };
146
288
  }
147
- function createUserMessageNode(piboSessionId, entry, content, entryIndex) {
289
+ function createUserMessageNode(piboSessionId, entry, content, entryIndex, timing) {
148
290
  const text = extractText(content);
149
291
  const notification = parseRunNotificationText(text);
150
292
  if (notification) {
@@ -159,8 +301,10 @@ function createUserMessageNode(piboSessionId, entry, content, entryIndex) {
159
301
  notification,
160
302
  });
161
303
  }
304
+ const userMessageType = timing?.userMessageType ?? "message_queued";
305
+ const eventIdentity = timing ? `event:${userMessageType}:${timing.eventId}` : undefined;
162
306
  return {
163
- id: `entry:${entry.id}`,
307
+ id: eventIdentity ?? `entry:${entry.id}`,
164
308
  entryId: entry.id,
165
309
  piboSessionId,
166
310
  type: "user.message",
@@ -170,7 +314,7 @@ function createUserMessageNode(piboSessionId, entry, content, entryIndex) {
170
314
  summary: text,
171
315
  output: text,
172
316
  source: "transcript",
173
- stableKey: `entry:${entry.id}`,
317
+ stableKey: eventIdentity ?? `entry:${entry.id}`,
174
318
  orderKey: transcriptTraceOrder(entryIndex, 0, "user.message"),
175
319
  children: [],
176
320
  };
@@ -181,7 +325,8 @@ function createAssistantTurnNodes(piboSessionId, entries, timing) {
181
325
  return [];
182
326
  const orderedNodes = [];
183
327
  const toolsByCallId = new Map();
184
- let assistantIndex = 0;
328
+ let assistantPartOrdinal = 0;
329
+ let reasoningPartOrdinal = 0;
185
330
  for (const { entry, index: entryIndex } of entries) {
186
331
  if (messageRole(entry) === "toolResult") {
187
332
  mergePersistedToolResult(toolsByCallId, orderedNodes, entry, piboSessionId, entryIndex);
@@ -193,16 +338,28 @@ function createAssistantTurnNodes(piboSessionId, entries, timing) {
193
338
  for (const [index, part] of messageParts(entry).entries()) {
194
339
  const typed = part;
195
340
  if (typed.type === "thinking" && typeof typed.thinking === "string" && hasVisibleText(typed.thinking)) {
196
- orderedNodes.push(createReasoningNode(piboSessionId, entry, entryIndex, index, typed.thinking));
341
+ const thinkingIndex = timing?.reasoningIndices?.[reasoningPartOrdinal] ?? reasoningPartOrdinal;
342
+ orderedNodes.push(createReasoningNode({
343
+ piboSessionId,
344
+ entry,
345
+ entryIndex,
346
+ contentPartIndex: index,
347
+ thinkingIndex,
348
+ eventId: timing?.eventId,
349
+ thinking: typed.thinking,
350
+ }));
351
+ reasoningPartOrdinal += 1;
197
352
  }
198
353
  else if (typed.type === "text" && typeof typed.text === "string" && typed.text !== "") {
199
354
  if (!responseNode) {
355
+ const assistantIndex = timing?.assistantIndices?.[assistantPartOrdinal] ?? assistantPartOrdinal;
200
356
  responseNode = createAssistantMessageNode({
201
- id: `entry:${entry.id}:response`,
202
357
  piboSessionId,
203
358
  entry,
204
359
  entryIndex,
205
360
  contentPartIndex: index,
361
+ assistantIndex,
362
+ eventId: timing?.eventId,
206
363
  status: responseStatus,
207
364
  text: typed.text,
208
365
  error: responseError,
@@ -225,9 +382,7 @@ function createAssistantTurnNodes(piboSessionId, entries, timing) {
225
382
  if (responseNode) {
226
383
  responseNode.status = responseStatus;
227
384
  responseNode.error = responseError;
228
- if (timing?.eventId)
229
- responseNode.stableKey = `assistant:${timing.eventId}:assistant:${assistantIndex}`;
230
- assistantIndex += 1;
385
+ assistantPartOrdinal += 1;
231
386
  }
232
387
  }
233
388
  const finalNode = orderedNodes.at(-1);
@@ -237,20 +392,21 @@ function createAssistantTurnNodes(piboSessionId, entries, timing) {
237
392
  }
238
393
  return orderedNodes;
239
394
  }
240
- function createReasoningNode(piboSessionId, entry, entryIndex, index, thinking) {
395
+ function createReasoningNode(input) {
396
+ const eventIdentity = input.eventId ? `${input.eventId}:thinking:${input.thinkingIndex}` : undefined;
241
397
  return {
242
- id: `entry:${entry.id}:thinking:${index}`,
243
- entryId: entry.id,
244
- piboSessionId,
398
+ id: eventIdentity ? thinkingNodeId(eventIdentity) : `entry:${input.entry.id}:thinking:${input.contentPartIndex}`,
399
+ entryId: input.entry.id,
400
+ piboSessionId: input.piboSessionId,
245
401
  type: "model.reasoning",
246
402
  title: "Thinking",
247
403
  status: "done",
248
- startedAt: entry.timestamp,
249
- summary: thinking,
250
- output: thinking,
404
+ startedAt: input.entry.timestamp,
405
+ summary: input.thinking,
406
+ output: input.thinking,
251
407
  source: "transcript",
252
- stableKey: `entry:${entry.id}:thinking:${index}`,
253
- orderKey: transcriptTraceOrder(entryIndex, index, "model.reasoning"),
408
+ stableKey: eventIdentity ? `reasoning:${eventIdentity}` : `entry:${input.entry.id}:thinking:${input.contentPartIndex}`,
409
+ orderKey: transcriptTraceOrder(input.entryIndex, input.contentPartIndex, "model.reasoning"),
254
410
  children: [],
255
411
  };
256
412
  }
@@ -258,7 +414,7 @@ function createToolCallNode(piboSessionId, entry, entryIndex, contentPartIndex,
258
414
  const name = typeof part.name === "string" ? part.name : "Tool Call";
259
415
  const toolCallId = typeof part.id === "string" ? part.id : undefined;
260
416
  return {
261
- id: `entry:${entry.id}:tool:${String(part.id)}`,
417
+ id: toolCallId ? `tool:${toolCallId}` : `entry:${entry.id}:tool:${contentPartIndex}`,
262
418
  entryId: entry.id,
263
419
  piboSessionId,
264
420
  toolCallId,
@@ -293,7 +449,7 @@ function mergePersistedToolResult(toolsByCallId, childNodes, entry, piboSessionI
293
449
  function createMissingToolResultNode(piboSessionId, entry, entryIndex, toolCallId) {
294
450
  const message = entry.message;
295
451
  return {
296
- id: `entry:${entry.id}:tool-result:${toolCallId}`,
452
+ id: `tool:${toolCallId}`,
297
453
  entryId: entry.id,
298
454
  piboSessionId,
299
455
  toolCallId,
@@ -313,10 +469,12 @@ function toolResultOutput(message) {
313
469
  return { content: message.content, details: message.details };
314
470
  }
315
471
  function createAssistantMessageNode(input) {
472
+ const eventIdentity = input.eventId ? `${input.eventId}:assistant:${input.assistantIndex}` : undefined;
316
473
  return {
317
- id: input.id,
474
+ id: eventIdentity ? assistantMessageNodeId(eventIdentity) : `entry:${input.entry.id}:response`,
318
475
  entryId: input.entry.id,
319
476
  piboSessionId: input.piboSessionId,
477
+ eventId: input.eventId,
320
478
  type: "assistant.message",
321
479
  title: "Agent Message",
322
480
  status: input.status,
@@ -326,7 +484,7 @@ function createAssistantMessageNode(input) {
326
484
  output: input.text,
327
485
  error: input.error,
328
486
  source: "transcript",
329
- stableKey: `entry:${input.entry.id}:response:${input.contentPartIndex}`,
487
+ stableKey: eventIdentity ? `assistant:${eventIdentity}` : `entry:${input.entry.id}:response:${input.contentPartIndex}`,
330
488
  orderKey: transcriptTraceOrder(input.entryIndex, input.contentPartIndex, "assistant.message"),
331
489
  children: input.children ?? [],
332
490
  };
@@ -1,5 +1,7 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  export const DEFAULT_CDP_URL = "http://127.0.0.1:56663";
2
3
  export const DEFAULT_CDP_TIMEOUT_MS = 2_500;
4
+ const CDP_JSON_CHUNK_SIZE = 256 * 1024;
3
5
  export class CdpClient {
4
6
  webSocketUrl;
5
7
  nextId = 0;
@@ -32,10 +34,11 @@ export class CdpClient {
32
34
  socket.addEventListener("open", succeed);
33
35
  socket.addEventListener("message", (event) => this.handleMessage(String(event.data)));
34
36
  socket.addEventListener("error", () => fail(new Error(`CDP WebSocket error connecting to ${this.webSocketUrl}`)));
35
- socket.addEventListener("close", () => {
37
+ socket.addEventListener("close", (event) => {
38
+ const detail = `code ${event.code}${event.reason ? `: ${event.reason}` : ""}`;
36
39
  for (const [id, pending] of this.pending) {
37
40
  clearTimeout(pending.timer);
38
- pending.reject(new Error("CDP target closed"));
41
+ pending.reject(new Error(`CDP target closed during ${pending.method} (${detail})`));
39
42
  this.pending.delete(id);
40
43
  }
41
44
  });
@@ -51,11 +54,17 @@ export class CdpClient {
51
54
  this.pending.delete(id);
52
55
  reject(new Error(`Timed out waiting for CDP method ${method}`));
53
56
  }, timeoutMs);
54
- this.pending.set(id, { resolve, reject, timer });
57
+ this.pending.set(id, { method, resolve, reject, timer });
55
58
  });
56
59
  this.socket.send(JSON.stringify(payload));
57
60
  return promise;
58
61
  }
62
+ sendDetached(method, params) {
63
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN)
64
+ return;
65
+ const id = ++this.nextId;
66
+ this.socket.send(JSON.stringify(params ? { id, method, params } : { id, method }));
67
+ }
59
68
  async evaluate(expression, timeoutMs = 10_000) {
60
69
  const response = await this.send("Runtime.evaluate", {
61
70
  expression,
@@ -67,6 +76,72 @@ export class CdpClient {
67
76
  throw new Error(`Browser evaluation failed: ${JSON.stringify(response.exceptionDetails)}`);
68
77
  return response.result?.value;
69
78
  }
79
+ async evaluateJson(expression, timeoutMs = 10_000) {
80
+ const deadlineMs = Date.now() + timeoutMs;
81
+ const cleanupReserveMs = Math.min(100, Math.max(1, Math.floor(timeoutMs / 10)));
82
+ const transferDeadlineMs = deadlineMs - cleanupReserveMs;
83
+ const storageKey = `__piboCdpJsonResult_${randomUUID()}`;
84
+ const keyLiteral = JSON.stringify(storageKey);
85
+ try {
86
+ const descriptor = await this.evaluate(`(async () => {
87
+ const deadlineMs = ${deadlineMs};
88
+ if (Date.now() >= deadlineMs) throw new Error("CDP JSON evaluation exceeded its deadline");
89
+ const state = { cancelled: false, json: undefined, cleanupTimer: undefined };
90
+ Object.defineProperty(globalThis, ${keyLiteral}, { value: state, configurable: true });
91
+ state.cleanupTimer = setTimeout(() => {
92
+ state.cancelled = true;
93
+ if (globalThis[${keyLiteral}] === state) delete globalThis[${keyLiteral}];
94
+ }, Math.max(0, deadlineMs - Date.now()));
95
+ try {
96
+ const value = await (${expression});
97
+ const json = JSON.stringify(value);
98
+ if (typeof json !== "string") throw new Error("CDP evaluation result is not JSON serializable");
99
+ if (Date.now() >= deadlineMs || state.cancelled || globalThis[${keyLiteral}] !== state) throw new Error("CDP JSON evaluation exceeded its deadline");
100
+ state.json = json;
101
+ return { length: json.length };
102
+ } catch (error) {
103
+ state.cancelled = true;
104
+ clearTimeout(state.cleanupTimer);
105
+ if (globalThis[${keyLiteral}] === state) delete globalThis[${keyLiteral}];
106
+ throw error;
107
+ }
108
+ })()`, remainingTime(transferDeadlineMs, "evaluating JSON through CDP"));
109
+ if (!Number.isInteger(descriptor?.length) || descriptor.length < 0)
110
+ throw new Error("CDP evaluation returned an invalid JSON result length");
111
+ const chunks = [];
112
+ for (let offset = 0; offset < descriptor.length; offset += CDP_JSON_CHUNK_SIZE) {
113
+ const end = Math.min(descriptor.length, offset + CDP_JSON_CHUNK_SIZE);
114
+ chunks.push(await this.evaluate(`(() => {
115
+ const state = globalThis[${keyLiteral}];
116
+ if (!state || typeof state.json !== "string") throw new Error("CDP JSON result storage is unavailable");
117
+ return state.json.slice(${offset}, ${end});
118
+ })()`, remainingTime(transferDeadlineMs, "retrieving JSON chunks through CDP")));
119
+ }
120
+ return JSON.parse(chunks.join(""));
121
+ }
122
+ finally {
123
+ const cleanupExpression = `(() => {
124
+ const state = globalThis[${keyLiteral}];
125
+ if (state && typeof state === "object") {
126
+ state.cancelled = true;
127
+ clearTimeout(state.cleanupTimer);
128
+ }
129
+ return delete globalThis[${keyLiteral}];
130
+ })()`;
131
+ const cleanupTimeoutMs = remainingTimeOrZero(deadlineMs);
132
+ if (cleanupTimeoutMs > 0) {
133
+ await this.evaluate(cleanupExpression, cleanupTimeoutMs).catch(() => undefined);
134
+ }
135
+ else {
136
+ this.sendDetached("Runtime.evaluate", {
137
+ expression: cleanupExpression,
138
+ awaitPromise: true,
139
+ returnByValue: true,
140
+ userGesture: true,
141
+ });
142
+ }
143
+ }
144
+ }
70
145
  close() {
71
146
  try {
72
147
  this.socket?.close();
@@ -187,3 +262,12 @@ function optionalStringValue(value) {
187
262
  const text = stringValue(value);
188
263
  return text || undefined;
189
264
  }
265
+ function remainingTime(deadlineMs, operation) {
266
+ const remainingMs = remainingTimeOrZero(deadlineMs);
267
+ if (remainingMs <= 0)
268
+ throw new Error(`Timed out ${operation}`);
269
+ return remainingMs;
270
+ }
271
+ function remainingTimeOrZero(deadlineMs) {
272
+ return Math.max(0, Math.floor(deadlineMs - Date.now()));
273
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "1.11.4",
3
+ "version": "1.12.1",
4
4
  "type": "module",
5
5
  "imports": {
6
6
  "vscode": "./src/apps/chat-vscode/extension/src/vscode-shim.js"