@pasko70/pibo 1.8.2 → 1.9.2

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 (40) hide show
  1. package/dist/apps/chat/data/chat-data-mappers.js +8 -0
  2. package/dist/apps/chat/data/project-service.js +23 -0
  3. package/dist/apps/chat/static-assets.js +6 -5
  4. package/dist/apps/chat-ui/assets/{dist--lraqdDn.js → dist-2KdPXbMT.js} +1 -1
  5. package/dist/apps/chat-ui/assets/{dist-Dgz3iWay.js → dist-5GM30SQK.js} +1 -1
  6. package/dist/apps/chat-ui/assets/{dist-CMFjl7MX.js → dist-9lsp1UpA.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-6pcjNbPQ.js → dist-BWbWIOcD.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-COlFTw2x.js → dist-BYKMZlI0.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-IlQbTNzw.js → dist-C-5u2QIS.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-bLd-NaVv.js → dist-Cge8JklW.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-CoCvyd4f.js → dist-CjYtD7ZT.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-CZSEM5So.js → dist-DFhhiR8M.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-YdEosvsr.js → dist-Etxmpyxg.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-CajLLpaw.js → dist-Idz5kzy8.js} +1 -1
  15. package/dist/apps/chat-ui/assets/index-BqD_bm7z.js +173 -0
  16. package/dist/apps/chat-ui/assets/index-C0x9nEcf.css +1 -0
  17. package/dist/apps/chat-ui/index.html +2 -2
  18. package/dist/apps/chat-vscode-web/assets/index-Cst9OUkC.js +41 -0
  19. package/dist/apps/chat-vscode-web/index.html +1 -1
  20. package/dist/cli.js +17 -0
  21. package/dist/compute/resource-health.js +12 -11
  22. package/dist/gateway/server.js +14 -1
  23. package/dist/gateway/web.js +2 -1
  24. package/dist/plugins/context-files.js +4 -2
  25. package/dist/ralph/store.js +1 -1
  26. package/dist/resources/cli.js +135 -0
  27. package/dist/resources/lifecycle.js +353 -0
  28. package/dist/resources/reaper-state.js +94 -0
  29. package/dist/resources/reaper.js +145 -0
  30. package/dist/session-ui/activeTurn.js +126 -0
  31. package/dist/session-ui/delegation.js +89 -0
  32. package/dist/session-ui/index.js +1 -0
  33. package/dist/session-ui/terminalRows.js +61 -0
  34. package/dist/shared/trace-engine.js +3 -2
  35. package/dist/shared/trace-event-projection.js +124 -14
  36. package/dist/shared/trace-transcript.js +73 -5
  37. package/package.json +1 -1
  38. package/dist/apps/chat-ui/assets/index-DbRZGRDd.css +0 -1
  39. package/dist/apps/chat-ui/assets/index-HPWlrJwv.js +0 -173
  40. package/dist/apps/chat-vscode-web/assets/index-Dge6XEYB.js +0 -41
@@ -0,0 +1,126 @@
1
+ export const EMPTY_STABLE_ACTIVE_TURN = { active: false };
2
+ export function resolveStableActiveTurn(previous, observation) {
3
+ const sessionId = observation.sessionId ?? previous.sessionId;
4
+ const sameSession = !previous.sessionId || !sessionId || previous.sessionId === sessionId;
5
+ const current = sameSession ? previous : { active: false, sessionId };
6
+ const terminalKey = observation.terminal?.key;
7
+ const observedStartedAt = validTimestamp(observation.startedAt) ? observation.startedAt : undefined;
8
+ if (observedStartedAt && terminalClosesTurn(observation.terminal, observedStartedAt)) {
9
+ return inactiveState(current, sessionId, terminalKey);
10
+ }
11
+ if (observedStartedAt) {
12
+ if (current.active &&
13
+ current.sessionId === sessionId &&
14
+ current.startedAt === observedStartedAt &&
15
+ current.terminalBaselineKey === terminalKey)
16
+ return current;
17
+ return {
18
+ sessionId,
19
+ active: true,
20
+ startedAt: observedStartedAt,
21
+ terminalBaselineKey: terminalKey,
22
+ };
23
+ }
24
+ if (current.active) {
25
+ if (terminalKey && terminalKey !== current.terminalBaselineKey) {
26
+ return inactiveState(current, sessionId, terminalKey);
27
+ }
28
+ return current;
29
+ }
30
+ if (observation.activeEvidence && terminalKey !== current.endedByTerminalKey) {
31
+ return {
32
+ sessionId,
33
+ active: true,
34
+ terminalBaselineKey: terminalKey,
35
+ };
36
+ }
37
+ if (current.sessionId === sessionId)
38
+ return current;
39
+ return { ...current, sessionId };
40
+ }
41
+ export function findLatestActiveTurnTerminal(traceView) {
42
+ if (!traceView)
43
+ return undefined;
44
+ let latest;
45
+ for (const event of traceView.rawEvents) {
46
+ const payload = record(event.payload);
47
+ const type = typeof payload?.type === "string" ? payload.type : event.type;
48
+ const isTerminal = type === "message_finished" || type === "session_error" ||
49
+ (type === "execution_result" && isTurnStoppingAction(payload?.action));
50
+ if (!isTerminal)
51
+ continue;
52
+ latest = latestActiveTurnTerminal(latest, {
53
+ key: `${type}:${event.eventSequence ?? event.streamId ?? event.id}`,
54
+ at: event.createdAt,
55
+ });
56
+ }
57
+ for (const node of flattenTraceNodes(traceView.nodes)) {
58
+ const terminalAt = node.type === "agent.turn" ? node.completedAt : node.type === "error" ? node.completedAt ?? node.startedAt : undefined;
59
+ if (!terminalAt)
60
+ continue;
61
+ latest = latestActiveTurnTerminal(latest, { key: `node:${node.id}:${node.status}`, at: terminalAt });
62
+ }
63
+ return latest;
64
+ }
65
+ export function findSignalActiveTurnStartedAt(sessionSignal, signalTree) {
66
+ if (!sessionSignal?.isTreeActive || !signalTree)
67
+ return undefined;
68
+ const currentTurn = sessionSignal.currentTurnId ? signalTree.nodes[sessionSignal.currentTurnId] : undefined;
69
+ if (currentTurn?.startedAt)
70
+ return currentTurn.startedAt;
71
+ return Object.values(signalTree.nodes)
72
+ .filter((node) => node.piboSessionId === sessionSignal.piboSessionId && node.kind === "turn" && isActiveSignalStatus(node.status) && node.startedAt)
73
+ .sort((left, right) => Date.parse(left.startedAt ?? left.createdAt) - Date.parse(right.startedAt ?? right.createdAt))
74
+ .at(-1)?.startedAt;
75
+ }
76
+ export function findSignalActiveTurnTerminal(sessionSignal) {
77
+ if (!sessionSignal || sessionSignal.isTreeActive)
78
+ return undefined;
79
+ const status = [sessionSignal.localStatus, sessionSignal.aggregateStatus].find(isTerminalSignalStatus);
80
+ return status ? { key: `signal:${status}:${sessionSignal.updatedAt}`, at: sessionSignal.updatedAt } : undefined;
81
+ }
82
+ export function latestActiveTurnTerminal(left, right) {
83
+ if (!left)
84
+ return right;
85
+ if (!right)
86
+ return left;
87
+ return Date.parse(right.at) >= Date.parse(left.at) ? right : left;
88
+ }
89
+ function flattenTraceNodes(nodes) {
90
+ return nodes.flatMap((node) => [node, ...flattenTraceNodes(node.children)]);
91
+ }
92
+ function inactiveState(current, sessionId, terminalKey) {
93
+ if (!current.active &&
94
+ current.sessionId === sessionId &&
95
+ current.endedByTerminalKey === terminalKey)
96
+ return current;
97
+ return {
98
+ sessionId,
99
+ active: false,
100
+ endedByTerminalKey: terminalKey,
101
+ };
102
+ }
103
+ function terminalClosesTurn(terminal, startedAt) {
104
+ if (!terminal)
105
+ return false;
106
+ const terminalMs = Date.parse(terminal.at);
107
+ const startedMs = Date.parse(startedAt);
108
+ return Number.isFinite(terminalMs) && Number.isFinite(startedMs) && terminalMs >= startedMs;
109
+ }
110
+ function isTurnStoppingAction(value) {
111
+ return value === "abort" || value === "kill" || value === "kill_all" || value === "dispose";
112
+ }
113
+ function isActiveSignalStatus(status) {
114
+ return status === "queued" || status === "starting" || status === "running" || status === "streaming" || status === "compacting" || status === "blocked" || status === "paused";
115
+ }
116
+ function isTerminalSignalStatus(status) {
117
+ return status === "error" || status === "failed" || status === "cancelled" || status === "interrupted" || status === "disposed";
118
+ }
119
+ function validTimestamp(value) {
120
+ return Boolean(value && Number.isFinite(Date.parse(value)));
121
+ }
122
+ function record(value) {
123
+ return value !== null && typeof value === "object" && !Array.isArray(value)
124
+ ? value
125
+ : undefined;
126
+ }
@@ -0,0 +1,89 @@
1
+ export function resolveAgentDelegationStatus(childSignal, fallbackStatus = "done", traceTerminal = false) {
2
+ if (!childSignal)
3
+ return fallbackStatus === "running" ? "running" : fallbackStatus === "error" ? "failed" : "completed";
4
+ if (traceTerminal)
5
+ return fallbackStatus === "error" ? "failed" : "completed";
6
+ const statuses = [childSignal.localStatus, childSignal.aggregateStatus];
7
+ if (statuses.some(isCancelledSignalStatus))
8
+ return "cancelled";
9
+ if (childSignal.hasError || childSignal.hasErrorDescendant || statuses.some(isFailedSignalStatus))
10
+ return "failed";
11
+ if (childSignal.isSettled === false || childSignal.isTreeActive || childSignal.isLocalActive || childSignal.hasActiveDescendant || statuses.some(isRunningSignalStatus))
12
+ return "running";
13
+ if (fallbackStatus === "running" && statuses.every(isIdleSignalStatus))
14
+ return "running";
15
+ return "completed";
16
+ }
17
+ export function extractAgentDelegationName(input, title, summary) {
18
+ const record = isRecord(input) ? input : undefined;
19
+ const rawName = stringValue(record?.subagentName)
20
+ ?? subagentNameFromToolName(title)
21
+ ?? stringValue(summary)
22
+ ?? stringValue(title)
23
+ ?? "Subagent";
24
+ return titleCaseAgentName(rawName);
25
+ }
26
+ export function extractAgentDelegationTask(input) {
27
+ if (typeof input === "string")
28
+ return nonEmpty(input);
29
+ if (!isRecord(input))
30
+ return undefined;
31
+ for (const key of ["message", "task", "prompt", "query"]) {
32
+ const value = nonEmpty(input[key]);
33
+ if (value)
34
+ return value;
35
+ }
36
+ return extractAgentDelegationTask(input.arguments);
37
+ }
38
+ export function compactAgentDelegationTask(input, maxLength = 180) {
39
+ const task = extractAgentDelegationTask(input)?.replace(/\s+/g, " ").trim();
40
+ if (!task || maxLength < 1)
41
+ return undefined;
42
+ return task.length <= maxLength ? task : `${task.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
43
+ }
44
+ export function formatAgentDelegationDuration(durationMs) {
45
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
46
+ if (totalSeconds < 60)
47
+ return `${totalSeconds}s`;
48
+ const hours = Math.floor(totalSeconds / 3600);
49
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
50
+ const seconds = totalSeconds % 60;
51
+ if (hours > 0)
52
+ return `${hours}h ${minutes}m ${seconds}s`;
53
+ return `${minutes}m ${seconds}s`;
54
+ }
55
+ function isRunningSignalStatus(status) {
56
+ return ["queued", "starting", "running", "streaming", "waiting", "blocked", "retrying", "compacting", "pausing", "paused"].includes(status);
57
+ }
58
+ function isIdleSignalStatus(status) {
59
+ return status === "idle" || status === "unknown";
60
+ }
61
+ function isFailedSignalStatus(status) {
62
+ return status === "error";
63
+ }
64
+ function isCancelledSignalStatus(status) {
65
+ return status === "cancelled" || status === "interrupted" || status === "disposed";
66
+ }
67
+ function subagentNameFromToolName(value) {
68
+ const name = nonEmpty(value);
69
+ if (!name)
70
+ return undefined;
71
+ return name.replace(/^pibo_subagent_/, "");
72
+ }
73
+ function titleCaseAgentName(value) {
74
+ return value
75
+ .replace(/^pibo_subagent_/, "")
76
+ .split(/[\s_-]+/)
77
+ .filter(Boolean)
78
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
79
+ .join(" ") || "Subagent";
80
+ }
81
+ function nonEmpty(value) {
82
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
83
+ }
84
+ function stringValue(value) {
85
+ return nonEmpty(value);
86
+ }
87
+ function isRecord(value) {
88
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
89
+ }
@@ -1,5 +1,6 @@
1
1
  export * from "./commandCatalog.js";
2
2
  export * from "./commandResults.js";
3
+ export * from "./delegation.js";
3
4
  export * from "./roomSessionViewModel.js";
4
5
  export * from "./statusViewModel.js";
5
6
  export * from "./terminalCards.js";
@@ -5,12 +5,43 @@ export const COMPACT_TERMINAL_EXPLORING_PREVIEW_LINES = 6;
5
5
  export function buildCompactTerminalRows(traceView, options) {
6
6
  if (!traceView)
7
7
  return [];
8
+ const turnById = mapTurnNodes(traceView.nodes);
8
9
  const flatNodes = flattenTraceNodes(traceView.nodes)
9
10
  .sort((left, right) => compareTraceNodes(left.node, right.node))
10
11
  .filter((item) => item.node.type !== "agent.turn" && (options.showThinking || item.node.type !== "model.reasoning"));
11
12
  const candidates = syncThinkingToolRows(flatNodes.map((item) => createRowCandidate(item.node, item.turnId)));
13
+ applyCompletedTurnTiming(candidates, turnById);
12
14
  return groupRelatedToolCandidates(candidates).map((candidate) => candidate.row);
13
15
  }
16
+ export function findActiveTurnStartedAt(traceView) {
17
+ if (!traceView)
18
+ return undefined;
19
+ const terminalErrorEventIds = new Set(flattenTraceNodes(traceView.nodes)
20
+ .map(({ node }) => node)
21
+ .filter((node) => node.type === "error" && node.eventId)
22
+ .map((node) => node.eventId));
23
+ return [...mapTurnNodes(traceView.nodes).values()]
24
+ .filter((turn) => turn.startedAt && !turn.completedAt && (!turn.eventId || !terminalErrorEventIds.has(turn.eventId)))
25
+ .sort(compareTraceNodes)
26
+ .at(-1)?.startedAt;
27
+ }
28
+ export function formatTerminalDuration(durationMs) {
29
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
30
+ const hours = Math.floor(totalSeconds / 3600);
31
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
32
+ const seconds = totalSeconds % 60;
33
+ return [hours, minutes, seconds].map((value) => String(value).padStart(2, "0")).join(":");
34
+ }
35
+ function mapTurnNodes(nodes) {
36
+ const turns = new Map();
37
+ for (const node of nodes) {
38
+ if (node.type === "agent.turn")
39
+ turns.set(node.id, node);
40
+ for (const [id, turn] of mapTurnNodes(node.children))
41
+ turns.set(id, turn);
42
+ }
43
+ return turns;
44
+ }
14
45
  function flattenTraceNodes(nodes, turnId) {
15
46
  const result = [];
16
47
  for (const node of nodes) {
@@ -71,6 +102,26 @@ function createRowCandidate(node, turnId) {
71
102
  }
72
103
  return { ...candidate, row: { ...candidate.row, ...debugFields(node) } };
73
104
  }
105
+ function applyCompletedTurnTiming(candidates, turnById) {
106
+ for (const turn of turnById.values()) {
107
+ if (!turn.completedAt)
108
+ continue;
109
+ const turnCandidates = candidates.filter((candidate) => candidate.turnId === turn.id);
110
+ const finalCandidate = turnCandidates.at(-1);
111
+ if (finalCandidate?.row.kind !== "message.assistant" || finalCandidate.row.status === "running")
112
+ continue;
113
+ finalCandidate.row.startedAt = turn.startedAt;
114
+ finalCandidate.row.completedAt = turn.completedAt;
115
+ finalCandidate.row.durationMs = turn.durationMs ?? durationBetween(turn.startedAt, turn.completedAt);
116
+ }
117
+ }
118
+ function durationBetween(startedAt, completedAt) {
119
+ if (!startedAt || !completedAt)
120
+ return undefined;
121
+ const start = new Date(startedAt).getTime();
122
+ const end = new Date(completedAt).getTime();
123
+ return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : undefined;
124
+ }
74
125
  function debugFields(node) {
75
126
  return {
76
127
  eventId: node.eventId,
@@ -90,6 +141,7 @@ function createUserMessageRow(node) {
90
141
  lines: [{ prefix: "prompt", tokens: [token(text)] }],
91
142
  sourceNodeIds: [node.id],
92
143
  forkEntryId: node.entryId,
144
+ startedAt: node.startedAt,
93
145
  output: text,
94
146
  payloadRefs: node.payloadRefs,
95
147
  };
@@ -101,6 +153,9 @@ function createAssistantMessageRow(node) {
101
153
  status: mapStatus(node.status),
102
154
  lines: [],
103
155
  sourceNodeIds: [node.id],
156
+ startedAt: node.startedAt,
157
+ completedAt: node.source === "transcript" ? node.completedAt : undefined,
158
+ durationMs: node.source === "transcript" ? node.durationMs : undefined,
104
159
  output: stringValue(node.output) || stringValue(node.summary) || "",
105
160
  error: node.error,
106
161
  payloadRefs: node.payloadRefs,
@@ -261,6 +316,9 @@ function createDelegationRow(node) {
261
316
  id: node.id,
262
317
  kind: "agent.delegation",
263
318
  status: mapStatus(node.status),
319
+ errorKind: node.status === "error" ? "tool" : undefined,
320
+ title: node.title,
321
+ summary: node.summary,
264
322
  lines: [
265
323
  {
266
324
  prefix: "bullet",
@@ -270,6 +328,9 @@ function createDelegationRow(node) {
270
328
  ],
271
329
  sourceNodeIds: [node.id],
272
330
  linkedPiboSessionId: node.linkedPiboSessionId,
331
+ startedAt: node.startedAt,
332
+ completedAt: node.completedAt,
333
+ durationMs: node.durationMs,
273
334
  input: node.input,
274
335
  output: node.output,
275
336
  error: node.error,
@@ -1,5 +1,5 @@
1
1
  import { reconcileAsyncAgentRunStatuses } from "./trace-async-agent-runs.js";
2
- import { applySingleEventToNodes, contentDeltaPatchNodeId, dedupeTraceEvents, eventsCanAffectAsyncAgentRunStatus, findOpenTranscriptEventIds, isConfirmedUserMessageEcho, latestTraceStreamId, traceEventDedupeKey, } from "./trace-event-projection.js";
2
+ import { applySingleEventToNodes, contentDeltaPatchNodeId, dedupeTraceEvents, eventsCanAffectAsyncAgentRunStatus, findOpenTranscriptEventIds, isConfirmedUserMessageEcho, latestTraceStreamId, messageTurnTimingsFromEvents, reconcileTranscriptUserMessageTimestamps, traceEventDedupeKey, } from "./trace-event-projection.js";
3
3
  import { flattenTraceNodes, mapTraceNodesById, nestTraceNodes } from "./trace-nodes.js";
4
4
  import { nestMutableCopiedTraceNodes, shareUnchangedTraceNodes } from "./trace-patch-nodes.js";
5
5
  import { mapTraceChildSessionsByParent, mapTraceSubagentSessionLinks, } from "./trace-subagent-links.js";
@@ -14,7 +14,8 @@ export function buildTraceViewFromEvents(input) {
14
14
  const allEntries = input.transcriptEntries ?? [];
15
15
  const openTranscriptEventIds = findOpenTranscriptEventIds(events, sessionStatus);
16
16
  const entries = projectTranscriptEntries(allEntries, sessionStatus, openTranscriptEventIds);
17
- const nodes = traceNodesFromEntries(input.session.id, entries);
17
+ const nodes = traceNodesFromEntries(input.session.id, entries, messageTurnTimingsFromEvents(events));
18
+ reconcileTranscriptUserMessageTimestamps(nodes, events);
18
19
  const byId = mapTraceNodesById(nodes);
19
20
  const childByParent = mapTraceChildSessionsByParent(input.sessions ?? []);
20
21
  const linkedChildByToolCallId = mapTraceSubagentSessionLinks(events);
@@ -26,10 +26,8 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
26
26
  const existing = byId.get(node.id);
27
27
  if (existing) {
28
28
  mergeAssistantMessageEvent(existing, node);
29
- closeParentTurnForFinalAssistant(byId, existing);
30
29
  return;
31
30
  }
32
- closeParentTurnForFinalAssistant(byId, node);
33
31
  nodes.push(node);
34
32
  byId.set(node.id, node);
35
33
  return;
@@ -49,10 +47,8 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
49
47
  const existing = byId.get(node.id);
50
48
  if (existing) {
51
49
  mergeAssistantMessageEvent(existing, node);
52
- closeParentTurnForFinalAssistant(byId, existing);
53
50
  return;
54
51
  }
55
- closeParentTurnForFinalAssistant(byId, node);
56
52
  }
57
53
  if (node.type === "execution.compaction") {
58
54
  const existing = findLatestCompactionNode(nodes);
@@ -68,6 +64,13 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
68
64
  return;
69
65
  }
70
66
  }
67
+ if (node.type === "agent.delegation" && !node.toolCallId && node.linkedPiboSessionId) {
68
+ const existing = findLegacySubagentLinkTarget([...byId.values()], node);
69
+ if (existing) {
70
+ mergeSubagentSessionLink(existing, node);
71
+ return;
72
+ }
73
+ }
71
74
  if (node.toolCallId) {
72
75
  const existing = [...byId.values()].find((candidate) => candidate.toolCallId === node.toolCallId &&
73
76
  (candidate.type === "tool.call" || candidate.type === "agent.delegation"));
@@ -102,6 +105,7 @@ function assistantMessageNodeFromEvent(piboSessionId, event, createdAt, eventSeq
102
105
  title: "Agent Message",
103
106
  status: "done",
104
107
  startedAt: createdAt,
108
+ completedAt: createdAt,
105
109
  summary: event.text,
106
110
  output: event.text,
107
111
  source: "event-log",
@@ -165,6 +169,28 @@ export function contentDeltaPatchNodeId(event) {
165
169
  }
166
170
  return undefined;
167
171
  }
172
+ export function reconcileTranscriptUserMessageTimestamps(nodes, events) {
173
+ const transcriptUsers = nodes.filter((node) => node.type === "user.message" && node.source === "transcript");
174
+ let userCursor = 0;
175
+ for (const storedEvent of events) {
176
+ const event = storedEvent.payload;
177
+ if (event.type !== "message_queued" || event.source !== "user")
178
+ continue;
179
+ const eventId = typeof event.eventId === "string" ? event.eventId : storedEvent.eventId;
180
+ const text = typeof event.text === "string" ? event.text : undefined;
181
+ const matchIndex = transcriptUsers.findIndex((node, index) => {
182
+ if (index < userCursor)
183
+ return false;
184
+ if (eventId && (node.entryId === eventId || node.stableKey === `entry:${eventId}`))
185
+ return true;
186
+ return Boolean(text && traceNodeText(node) === text);
187
+ });
188
+ if (matchIndex === -1)
189
+ continue;
190
+ transcriptUsers[matchIndex].startedAt = storedEvent.createdAt;
191
+ userCursor = matchIndex + 1;
192
+ }
193
+ }
168
194
  export function isConfirmedUserMessageEcho(nodes, event) {
169
195
  const payload = event.payload;
170
196
  if (payload.type !== "message_queued" || payload.source !== "user")
@@ -266,6 +292,7 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
266
292
  ...base,
267
293
  id: eventId ? messageTurnNodeId(eventId) : id,
268
294
  type: "agent.turn",
295
+ startedAt: event.type === "message_started" ? createdAt : undefined,
269
296
  title: "Agent Turn",
270
297
  status: event.type === "message_finished" || sessionStatus !== "running" ? "done" : "running",
271
298
  completedAt: event.type === "message_finished" ? createdAt : undefined,
@@ -296,6 +323,7 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
296
323
  type: "assistant.message",
297
324
  title: "Agent Message",
298
325
  status: "done",
326
+ completedAt: createdAt,
299
327
  summary: event.text,
300
328
  output: event.text,
301
329
  stableKey: assistantId ? `assistant:${assistantId}` : base.stableKey,
@@ -483,15 +511,6 @@ function mergeReasoningEvent(target, update) {
483
511
  target.output = update.output ?? target.output;
484
512
  target.completedAt = update.completedAt ?? target.completedAt;
485
513
  }
486
- function closeParentTurnForFinalAssistant(byId, assistant) {
487
- if (!assistant.parentId || assistant.status !== "done")
488
- return;
489
- const parent = byId.get(assistant.parentId);
490
- if (!parent || parent.type !== "agent.turn")
491
- return;
492
- parent.status = "done";
493
- parent.completedAt = assistant.completedAt ?? assistant.startedAt ?? parent.completedAt;
494
- }
495
514
  function isInternalSessionOperation(action) {
496
515
  return action === "session.fork" || action === "session.clone" || action === "session.switch";
497
516
  }
@@ -512,6 +531,55 @@ function shouldKeepTranscriptEchoEvent(event, openTranscriptEventIds) {
512
531
  function isStaleToolCallEchoEvent(event, sessionStatus) {
513
532
  return sessionStatus !== "running" && event.type === "tool_call";
514
533
  }
534
+ export function messageTurnTimingsFromEvents(events) {
535
+ const timings = new Map();
536
+ const completedEventIds = [];
537
+ const completedEventIdSet = new Set();
538
+ const ignoredEventIds = new Set();
539
+ for (const storedEvent of events) {
540
+ const event = storedEvent.payload;
541
+ if (event.type !== "message_started" && event.type !== "message_finished")
542
+ continue;
543
+ const eventId = typeof event.eventId === "string" ? event.eventId : undefined;
544
+ if (!eventId)
545
+ continue;
546
+ if (event.type === "message_started" && event.source === "service") {
547
+ ignoredEventIds.add(eventId);
548
+ continue;
549
+ }
550
+ if (ignoredEventIds.has(eventId))
551
+ continue;
552
+ const timing = timings.get(eventId) ?? {};
553
+ if (event.type === "message_started") {
554
+ timing.userText ??= event.text;
555
+ timing.startedAt ??= storedEvent.createdAt;
556
+ }
557
+ else {
558
+ timing.completedAt = storedEvent.createdAt;
559
+ if (!completedEventIdSet.has(eventId)) {
560
+ completedEventIds.push(eventId);
561
+ completedEventIdSet.add(eventId);
562
+ }
563
+ }
564
+ timings.set(eventId, timing);
565
+ }
566
+ return completedEventIds.flatMap((eventId) => {
567
+ const timing = timings.get(eventId);
568
+ if (!timing?.completedAt)
569
+ return [];
570
+ const startedAtMs = parseTimestamp(timing.startedAt);
571
+ const completedAtMs = parseTimestamp(timing.completedAt);
572
+ return [{
573
+ eventId,
574
+ userText: timing.userText,
575
+ startedAt: timing.startedAt,
576
+ completedAt: timing.completedAt,
577
+ durationMs: startedAtMs === undefined || completedAtMs === undefined
578
+ ? undefined
579
+ : Math.max(0, completedAtMs - startedAtMs),
580
+ }];
581
+ });
582
+ }
515
583
  export function findOpenTranscriptEventIds(events, sessionStatus) {
516
584
  if (sessionStatus !== "running")
517
585
  return new Set();
@@ -626,12 +694,48 @@ function thinkingEventNodeId(event) {
626
694
  function mergeToolEvent(target, update) {
627
695
  target.status = update.status;
628
696
  target.summary = update.summary ?? target.summary;
629
- target.input = update.input ?? target.input;
697
+ target.input = mergeDelegationInput(target, update);
630
698
  target.output = update.output ?? target.output;
631
699
  target.error = update.error ?? target.error;
632
700
  target.completedAt = update.completedAt ?? target.completedAt;
633
701
  target.linkedPiboSessionId = update.linkedPiboSessionId ?? target.linkedPiboSessionId;
634
702
  }
703
+ function mergeSubagentSessionLink(target, update) {
704
+ target.summary = update.summary ?? target.summary;
705
+ target.input = mergeDelegationInput(target, update);
706
+ target.linkedPiboSessionId = update.linkedPiboSessionId ?? target.linkedPiboSessionId;
707
+ }
708
+ function findLegacySubagentLinkTarget(nodes, update) {
709
+ const delegations = [...nodes].reverse().filter((candidate) => candidate.type === "agent.delegation");
710
+ const agentName = delegationAgentName(update);
711
+ const candidates = delegations.filter((candidate) => !candidate.linkedPiboSessionId && delegationAgentName(candidate) === agentName);
712
+ const threadKey = delegationThreadKey(update.input);
713
+ const matchingCandidate = threadKey
714
+ ? candidates.find((candidate) => delegationThreadKey(candidate.input) === threadKey)
715
+ : candidates.length === 1 ? candidates[0] : undefined;
716
+ if (matchingCandidate)
717
+ return matchingCandidate;
718
+ return delegations.find((candidate) => candidate.linkedPiboSessionId === update.linkedPiboSessionId);
719
+ }
720
+ function delegationAgentName(node) {
721
+ const input = isObjectRecord(node.input) ? node.input : undefined;
722
+ const value = typeof input?.subagentName === "string" ? input.subagentName : node.summary ?? node.title;
723
+ return typeof value === "string" ? value.replace(/^pibo_subagent_/, "").trim().toLowerCase() || undefined : undefined;
724
+ }
725
+ function delegationThreadKey(value) {
726
+ if (!isObjectRecord(value) || typeof value.threadKey !== "string")
727
+ return undefined;
728
+ return value.threadKey.trim() || undefined;
729
+ }
730
+ function mergeDelegationInput(target, update) {
731
+ if (target.type !== "agent.delegation" || !isObjectRecord(target.input) || !isObjectRecord(update.input)) {
732
+ return update.input ?? target.input;
733
+ }
734
+ return Object.fromEntries(Object.entries({ ...target.input, ...update.input }).filter(([, value]) => value !== undefined));
735
+ }
736
+ function isObjectRecord(value) {
737
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
738
+ }
635
739
  function findLatestCompactionNode(nodes) {
636
740
  return flattenTraceNodes([...nodes]).reverse().find((node) => node.type === "execution.compaction" && node.status === "running");
637
741
  }
@@ -656,6 +760,12 @@ function stringifyPreview(value) {
656
760
  return String(value);
657
761
  }
658
762
  }
763
+ function parseTimestamp(value) {
764
+ if (!value)
765
+ return undefined;
766
+ const timestamp = new Date(value).getTime();
767
+ return Number.isFinite(timestamp) ? timestamp : undefined;
768
+ }
659
769
  function cryptoSafeId(value) {
660
770
  return base64UrlEncode(new TextEncoder().encode(JSON.stringify(value))).slice(0, 48);
661
771
  }