@pasko70/pibo 1.10.0 → 1.11.0

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 (72) hide show
  1. package/dist/apps/chat/chat-request-normalizers.js +7 -0
  2. package/dist/apps/chat/data/chat-data-mappers.js +2 -0
  3. package/dist/apps/chat/loop-api.js +11 -6
  4. package/dist/apps/chat/web-app.js +30 -10
  5. package/dist/apps/chat-ui/assets/{dist-wE9nop9V.js → dist-0E9FVJ6k.js} +1 -1
  6. package/dist/apps/chat-ui/assets/{dist-DlATLa-U.js → dist-BvqC0hRM.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-DUlaXAk7.js → dist-C2BRHNXr.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-nOLTkZrJ.js → dist-C57jNmjf.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-Y-AA2omI.js → dist-CBL8UXjd.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-D-cxLQO1.js → dist-CI-LPHjw.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-LHRs1Nhr.js → dist-CMRFkfl5.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-CS7wdk0Z.js → dist-CpZPhD2y.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-BwKObYnX.js → dist-CuCZ-3_K.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-GdEM8UW1.js → dist-CzC5kPWJ.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{dist-CKtT8YGm.js → dist-D9hLnn1T.js} +1 -1
  16. package/dist/apps/chat-ui/assets/index-BEvjPUor.js +173 -0
  17. package/dist/apps/chat-ui/assets/{index-C0x9nEcf.css → index-DNeE4HrG.css} +1 -1
  18. package/dist/apps/chat-ui/index.html +2 -2
  19. package/dist/apps/chat-vscode-web/assets/index-BLZRi4Ka.js +41 -0
  20. package/dist/apps/chat-vscode-web/assets/index-Bf2JvJ9z.css +2 -0
  21. package/dist/apps/chat-vscode-web/index.html +2 -2
  22. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  23. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.11.0.vsix +0 -0
  24. package/dist/auth/better-auth.js +49 -1
  25. package/dist/auth/cli.js +220 -0
  26. package/dist/auth/machine-keys.js +258 -0
  27. package/dist/auth/machine-session.js +123 -0
  28. package/dist/bin/pibo.js +0 -0
  29. package/dist/bin/rg.js +0 -0
  30. package/dist/cli-session/localSessionSource.js +34 -12
  31. package/dist/cli.js +6 -0
  32. package/dist/compute/cli.js +7 -0
  33. package/dist/compute/resource-health.js +61 -4
  34. package/dist/config/config.js +5 -0
  35. package/dist/core/events.js +6 -1
  36. package/dist/core/routed-session.js +67 -4
  37. package/dist/core/session-router.js +40 -6
  38. package/dist/data/ingest-service.js +3 -1
  39. package/dist/data/schema.js +4 -0
  40. package/dist/debug/index.js +4 -0
  41. package/dist/debug/trace-status.js +25 -0
  42. package/dist/debug/trace.js +51 -17
  43. package/dist/index.js +1 -0
  44. package/dist/loops/accounting.js +19 -0
  45. package/dist/loops/cli.js +22 -6
  46. package/dist/loops/prompts.js +4 -1
  47. package/dist/loops/service.js +97 -8
  48. package/dist/loops/store.js +128 -17
  49. package/dist/loops/tools.js +24 -7
  50. package/dist/reliability/store.js +34 -7
  51. package/dist/resources/cli.js +15 -0
  52. package/dist/resources/lifecycle.js +28 -4
  53. package/dist/resources/reaper.js +1 -0
  54. package/dist/runs/lifecycle.js +59 -0
  55. package/dist/runs/registry.js +47 -1
  56. package/dist/runs/tools.js +31 -13
  57. package/dist/session-ui/terminalRows.js +66 -2
  58. package/dist/shared/trace-async-agent-runs.js +4 -4
  59. package/dist/shared/trace-event-projection.js +59 -19
  60. package/dist/shared/trace-nodes.js +5 -0
  61. package/dist/shared/trace-page-merge.js +15 -0
  62. package/dist/shared/trace-run-notifications.js +3 -1
  63. package/dist/signals/projector.js +16 -3
  64. package/dist/tools/browser-pool.js +50 -0
  65. package/dist/tools/browser-use-leases.js +12 -8
  66. package/dist/tools/guides.js +8 -1
  67. package/dist/tools/index.js +1 -0
  68. package/package.json +2 -1
  69. package/skills/builtin/loop/SKILL.md +12 -3
  70. package/dist/apps/chat-ui/assets/index-8W_yMHQI.js +0 -173
  71. package/dist/apps/chat-vscode-web/assets/index-B5QK07zO.css +0 -2
  72. package/dist/apps/chat-vscode-web/assets/index-BAMxIaI_.js +0 -41
@@ -0,0 +1,59 @@
1
+ export class PiboRunExecutionTimeoutError extends Error {
2
+ timeoutPhase;
3
+ constructor(message, timeoutPhase) {
4
+ super(message);
5
+ this.timeoutPhase = timeoutPhase;
6
+ this.name = "PiboRunExecutionTimeoutError";
7
+ }
8
+ }
9
+ export function resolveRunTimeoutMs(toolName, params) {
10
+ if (!params || typeof params !== "object" || Array.isArray(params))
11
+ return undefined;
12
+ const input = params;
13
+ if (typeof input.timeoutMs === "number")
14
+ return normalizePositiveTimeout(input.timeoutMs, "arguments.timeoutMs");
15
+ if (toolName === "bash" && typeof input.timeout === "number")
16
+ return normalizePositiveTimeout(input.timeout * 1000, "arguments.timeout");
17
+ return undefined;
18
+ }
19
+ export function foregroundServiceWarning(toolName, params, timeoutMs) {
20
+ if (toolName !== "bash" || timeoutMs === undefined || !params || typeof params !== "object" || Array.isArray(params))
21
+ return undefined;
22
+ const command = typeof params.command === "string" ? String(params.command) : "";
23
+ if (!isKnownForegroundServiceCommand(command))
24
+ return undefined;
25
+ return `Known long-lived service command is running in a finite yielded run (${timeoutMs}ms). It can start successfully and later end as timed_out. Use the managed gateway CLI, or launch a detached/background service with a separate bounded health check.`;
26
+ }
27
+ export function isConfiguredTimeoutError(error) {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ return /(?:timed?\s*out|timeout)/i.test(message);
30
+ }
31
+ export function hasMeaningfulTimeoutOutput(value) {
32
+ const text = extractText(value);
33
+ if (!text)
34
+ return false;
35
+ return text.split(/\r?\n/).some((line) => {
36
+ const trimmed = line.trim();
37
+ return !!trimmed && !/(?:timed?\s*out|timeout|command exited with code|process exited with code)/i.test(trimmed);
38
+ });
39
+ }
40
+ function isKnownForegroundServiceCommand(command) {
41
+ return /(?:^|[;&|]\s*|\b)(?:pibo\s+gateway(?::web|\s+web|\s+dev)?(?:\s|$)|node\s+[^\n]*pibo(?:\.js)?\s+gateway:web(?:\s|$)|(?:\.\/)?(?:scripts\/)?docker-entrypoint\.sh\s+gateway:web(?:\s|$)|npm\s+(?:run\s+)?gateway(?::web)?(?:\s|$)|(?:npm\s+run\s+)?(?:dev|start)\s+--[^\n]*\bgateway(?::web)?\b|vite(?:\s|$))/i.test(command);
42
+ }
43
+ function normalizePositiveTimeout(value, field) {
44
+ if (!Number.isFinite(value) || value <= 0)
45
+ throw new Error(`${field} must be a positive number`);
46
+ return Math.max(1, Math.round(value));
47
+ }
48
+ function extractText(value) {
49
+ if (typeof value === "string")
50
+ return value;
51
+ if (!value || typeof value !== "object")
52
+ return "";
53
+ const candidate = value;
54
+ if (typeof candidate.text === "string")
55
+ return candidate.text;
56
+ if (!Array.isArray(candidate.content))
57
+ return "";
58
+ return candidate.content.map((part) => part && typeof part === "object" && typeof part.text === "string" ? String(part.text) : "").filter(Boolean).join("\n");
59
+ }
@@ -4,6 +4,14 @@ const DEFAULT_DETACHED_TERMINAL_TTL_MS = 60 * 1000;
4
4
  function now() {
5
5
  return new Date().toISOString();
6
6
  }
7
+ function runTimeoutAt(createdAt, timeoutMs) {
8
+ return timeoutMs === undefined ? undefined : new Date(Date.parse(createdAt) + timeoutMs).toISOString();
9
+ }
10
+ function formatTimeout(timeoutMs) {
11
+ if (timeoutMs === undefined)
12
+ return "its configured timeout";
13
+ return timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}s timeout` : `${timeoutMs}ms timeout`;
14
+ }
7
15
  function snapshot(record) {
8
16
  const output = {
9
17
  runId: record.runId,
@@ -18,12 +26,20 @@ function snapshot(record) {
18
26
  };
19
27
  if (record.summary)
20
28
  output.summary = record.summary;
29
+ if (record.timeoutMs !== undefined)
30
+ output.timeoutMs = record.timeoutMs;
31
+ if (record.timeoutAt)
32
+ output.timeoutAt = record.timeoutAt;
33
+ if (record.timeoutPhase)
34
+ output.timeoutPhase = record.timeoutPhase;
35
+ if (record.serviceWarning)
36
+ output.serviceWarning = record.serviceWarning;
21
37
  if (record.completedAt)
22
38
  output.completedAt = record.completedAt;
23
39
  return output;
24
40
  }
25
41
  function terminal(status) {
26
- return status === "completed" || status === "failed" || status === "cancelled";
42
+ return status === "completed" || status === "failed" || status === "timed_out" || status === "cancelled";
27
43
  }
28
44
  export class PiboRunRegistry {
29
45
  options;
@@ -53,6 +69,8 @@ export class PiboRunRegistry {
53
69
  params: input.params,
54
70
  retryable: input.retryable ?? false,
55
71
  maxAttempts: input.maxAttempts ?? 1,
72
+ timeoutMs: input.timeoutMs,
73
+ serviceWarning: input.serviceWarning,
56
74
  });
57
75
  const record = recordFromStored(stored);
58
76
  this.runs.set(record.runId, record);
@@ -75,6 +93,8 @@ export class PiboRunRegistry {
75
93
  summary: `${input.toolName} run is running.`,
76
94
  retryable: input.retryable ?? false,
77
95
  maxAttempts: Math.max(1, input.maxAttempts ?? 1),
96
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs, timeoutAt: runTimeoutAt(timestamp, input.timeoutMs) } : {}),
97
+ ...(input.serviceWarning ? { serviceWarning: input.serviceWarning } : {}),
78
98
  };
79
99
  this.runs.set(runId, record);
80
100
  const output = snapshot(record);
@@ -113,6 +133,25 @@ export class PiboRunRegistry {
113
133
  this.notify({ type: "run_changed", run: output, previousStatus, reason: error });
114
134
  return output;
115
135
  }
136
+ timeOut(runId, error, timeoutPhase) {
137
+ const record = this.runs.get(runId);
138
+ if (!record || terminal(record.status))
139
+ return undefined;
140
+ const previousStatus = record.status;
141
+ record.status = "timed_out";
142
+ record.error = error;
143
+ record.timeoutPhase = timeoutPhase;
144
+ record.summary = timeoutPhase === "lifetime"
145
+ ? `${record.toolName} run started successfully, then reached ${formatTimeout(record.timeoutMs)}.`
146
+ : `${record.toolName} run reached ${formatTimeout(record.timeoutMs)} before startup was confirmed.`;
147
+ this.finish(record);
148
+ this.options.store?.updateRun(runId, record);
149
+ if (record.jobId)
150
+ this.options.store?.fail(record.jobId, `run-registry:${process.pid}`, error);
151
+ const output = snapshot(record);
152
+ this.notify({ type: "run_changed", run: output, previousStatus, reason: error });
153
+ return output;
154
+ }
116
155
  list(controllerPiboSessionId, options = {}) {
117
156
  this.prune();
118
157
  return [...this.runs.values()]
@@ -219,6 +258,7 @@ export class PiboRunRegistry {
219
258
  const notification = {
220
259
  completed: [],
221
260
  failed: [],
261
+ timedOut: [],
222
262
  cancelled: [],
223
263
  running: [],
224
264
  };
@@ -228,6 +268,8 @@ export class PiboRunRegistry {
228
268
  notification.completed.push(item);
229
269
  else if (record.status === "failed")
230
270
  notification.failed.push(item);
271
+ else if (record.status === "timed_out")
272
+ notification.timedOut.push(item);
231
273
  else if (record.status === "cancelled")
232
274
  notification.cancelled.push(item);
233
275
  else
@@ -356,5 +398,9 @@ function recordFromStored(record) {
356
398
  jobId: record.jobId,
357
399
  retryable: record.retryable,
358
400
  maxAttempts: record.maxAttempts,
401
+ timeoutMs: record.timeoutMs,
402
+ timeoutAt: record.timeoutAt,
403
+ timeoutPhase: record.timeoutPhase,
404
+ serviceWarning: record.serviceWarning,
359
405
  };
360
406
  }
@@ -1,5 +1,6 @@
1
1
  import { StringEnum, Type } from "@earendil-works/pi-ai";
2
2
  import { defineTool } from "@earendil-works/pi-coding-agent";
3
+ import { foregroundServiceWarning, hasMeaningfulTimeoutOutput, isConfiguredTimeoutError, PiboRunExecutionTimeoutError, resolveRunTimeoutMs } from "./lifecycle.js";
3
4
  function resultText(prefix, value) {
4
5
  return `${prefix}\n${JSON.stringify(value, null, 2)}`;
5
6
  }
@@ -30,7 +31,7 @@ export function createRunToolDefinitions(yieldableTools, controller) {
30
31
  defineTool({
31
32
  name: "pibo_run_start",
32
33
  label: "Pibo Run Start",
33
- description: "Start a yieldable tool as a yielded run. Use tracked when the result may matter later; use detached only for intentional fire-and-forget work.",
34
+ description: "Start a yieldable tool as a yielded run. The run records its configured timeout and classifies lifetime expiry separately from command failure. Use detached only for intentional fire-and-forget work.",
34
35
  promptSnippet: "Use pibo_run_start to run a yieldable tool in the background. It returns a runId. Use pibo_run_read for completed results and pibo_run_wait/status/list/cancel/ack to manage runs.",
35
36
  executionMode: "parallel",
36
37
  parameters: Type.Object({
@@ -43,25 +44,42 @@ export function createRunToolDefinitions(yieldableTools, controller) {
43
44
  }),
44
45
  async execute(toolCallId, params, signal, onUpdate, ctx) {
45
46
  const tool = requireTool(yieldableTools, params.toolName);
47
+ const timeoutMs = resolveRunTimeoutMs(tool.name, params.arguments);
48
+ const serviceWarning = foregroundServiceWarning(tool.name, params.arguments, timeoutMs);
49
+ let observedOutput = false;
46
50
  const run = controller.startToolRun({
47
51
  toolName: tool.name,
48
52
  params: params.arguments,
49
53
  completionPolicy: params.completionPolicy,
54
+ timeoutMs,
55
+ serviceWarning,
50
56
  async execute() {
51
- const result = await tool.execute(toolCallId, params.arguments, signal, onUpdate, ctx);
52
- const resultObject = result;
53
- const text = textFromToolResult(resultObject);
54
- if (resultObject.isError === true) {
55
- throw new Error(text ?? `${tool.name} returned an error result.`);
57
+ try {
58
+ const result = await tool.execute(toolCallId, params.arguments, signal, (update) => {
59
+ observedOutput ||= hasMeaningfulTimeoutOutput(update);
60
+ onUpdate?.(update);
61
+ }, ctx);
62
+ const resultObject = result;
63
+ const text = textFromToolResult(resultObject);
64
+ if (resultObject.isError === true) {
65
+ if (timeoutMs !== undefined && isConfiguredTimeoutError(text ?? ""))
66
+ throw new PiboRunExecutionTimeoutError(text ?? `${tool.name} timed out.`, observedOutput || hasMeaningfulTimeoutOutput(text) ? "lifetime" : "startup");
67
+ throw new Error(text ?? `${tool.name} returned an error result.`);
68
+ }
69
+ return { text, details: resultObject.details ?? result };
70
+ }
71
+ catch (error) {
72
+ if (error instanceof PiboRunExecutionTimeoutError)
73
+ throw error;
74
+ if (timeoutMs !== undefined && isConfiguredTimeoutError(error))
75
+ throw new PiboRunExecutionTimeoutError(error instanceof Error ? error.message : String(error), observedOutput ? "lifetime" : "startup");
76
+ throw error;
56
77
  }
57
- return {
58
- text,
59
- details: resultObject.details ?? result,
60
- };
61
78
  },
62
79
  });
80
+ const prefix = serviceWarning ? `Started yielded run ${run.runId}.\nWarning: ${serviceWarning}` : `Started yielded run ${run.runId}.`;
63
81
  return {
64
- content: [{ type: "text", text: resultText(`Started yielded run ${run.runId}.`, run) }],
82
+ content: [{ type: "text", text: resultText(prefix, run) }],
65
83
  details: run,
66
84
  };
67
85
  },
@@ -132,8 +150,8 @@ export function createRunToolDefinitions(yieldableTools, controller) {
132
150
  defineTool({
133
151
  name: "pibo_run_read",
134
152
  label: "Pibo Run Read",
135
- description: "Read the terminal result or error for a yielded run.",
136
- promptSnippet: "Use pibo_run_read to retrieve a completed or failed run result. Reading terminal tracked runs consumes reminders.",
153
+ description: "Read the terminal result, timeout reason, or error for a yielded run.",
154
+ promptSnippet: "Use pibo_run_read to retrieve a completed, failed, or timed_out run result. Reading terminal tracked runs consumes reminders.",
137
155
  executionMode: "parallel",
138
156
  parameters: Type.Object({
139
157
  runId: Type.String({ description: "Run id returned by pibo_run_start" }),
@@ -11,7 +11,7 @@ export function buildCompactTerminalRows(traceView, options) {
11
11
  .filter((item) => item.node.type !== "agent.turn" && (options.showThinking || item.node.type !== "model.reasoning"));
12
12
  const candidates = syncThinkingToolRows(flatNodes.map((item) => createRowCandidate(item.node, item.turnId)));
13
13
  applyCompletedTurnTiming(candidates, turnById);
14
- return groupRelatedToolCandidates(candidates).map((candidate) => candidate.row);
14
+ return groupRelatedToolCandidates(reconcileConceptualRowCandidates(candidates)).map((candidate) => candidate.row);
15
15
  }
16
16
  export function findActiveTurnStartedAt(traceView) {
17
17
  if (!traceView)
@@ -100,7 +100,71 @@ function createRowCandidate(node, turnId) {
100
100
  };
101
101
  break;
102
102
  }
103
- return { ...candidate, row: { ...candidate.row, ...debugFields(node) } };
103
+ return {
104
+ ...candidate,
105
+ row: {
106
+ ...candidate.row,
107
+ id: compactTerminalRowIdentity(node),
108
+ ...debugFields(node),
109
+ },
110
+ };
111
+ }
112
+ export function compactTerminalRowIdentity(node) {
113
+ if (node.type === "tool.call" || node.type === "tool.result" || node.type === "agent.delegation") {
114
+ const toolCallId = node.toolCallId ?? stableKeySuffix(node.stableKey, "tool:");
115
+ if (toolCallId)
116
+ return `terminal:tool:${toolCallId}`;
117
+ if (node.type === "agent.delegation" && node.linkedPiboSessionId) {
118
+ return `terminal:delegation:${node.linkedPiboSessionId}`;
119
+ }
120
+ }
121
+ if ((node.type === "agent.async" || node.type === "yielded.run") && node.runId) {
122
+ return `terminal:run:${node.runId}`;
123
+ }
124
+ if (node.type === "assistant.message") {
125
+ const assistantId = stableKeySuffix(node.stableKey, "assistant:");
126
+ if (assistantId)
127
+ return `terminal:assistant:${assistantId}`;
128
+ }
129
+ if (node.type === "model.reasoning") {
130
+ const reasoningId = stableKeySuffix(node.stableKey, "reasoning:");
131
+ if (reasoningId)
132
+ return `terminal:reasoning:${reasoningId}`;
133
+ }
134
+ return node.id;
135
+ }
136
+ function stableKeySuffix(stableKey, prefix) {
137
+ if (!stableKey?.startsWith(prefix))
138
+ return undefined;
139
+ const suffix = stableKey.slice(prefix.length);
140
+ return suffix && suffix !== "undefined" ? suffix : undefined;
141
+ }
142
+ function reconcileConceptualRowCandidates(candidates) {
143
+ const reconciled = [];
144
+ const indexById = new Map();
145
+ for (const candidate of candidates) {
146
+ const existingIndex = indexById.get(candidate.row.id);
147
+ if (existingIndex === undefined) {
148
+ indexById.set(candidate.row.id, reconciled.length);
149
+ reconciled.push(candidate);
150
+ continue;
151
+ }
152
+ const existing = reconciled[existingIndex];
153
+ reconciled[existingIndex] = {
154
+ ...candidate,
155
+ turnId: candidate.turnId ?? existing.turnId,
156
+ row: {
157
+ ...existing.row,
158
+ ...candidate.row,
159
+ sourceNodeIds: [...new Set([...existing.row.sourceNodeIds, ...candidate.row.sourceNodeIds])],
160
+ input: candidate.row.input ?? existing.row.input,
161
+ output: candidate.row.output ?? existing.row.output,
162
+ error: candidate.row.error ?? existing.row.error,
163
+ payloadRefs: { ...existing.row.payloadRefs, ...candidate.row.payloadRefs },
164
+ },
165
+ };
166
+ }
167
+ return reconciled;
104
168
  }
105
169
  function applyCompletedTurnTiming(candidates, turnById) {
106
170
  for (const turn of turnById.values()) {
@@ -34,12 +34,12 @@ export function reconcileAsyncAgentRunStatuses(nodes) {
34
34
  if (!latest)
35
35
  continue;
36
36
  const status = stringValue(latest.snapshot.status);
37
- if (status !== "completed" && status !== "cancelled" && status !== "failed")
37
+ if (status !== "completed" && status !== "cancelled" && status !== "failed" && status !== "timed_out")
38
38
  continue;
39
- node.status = status === "failed" ? "error" : "done";
39
+ node.status = status === "failed" || status === "timed_out" ? "error" : "done";
40
40
  node.completedAt = latest.completedAt ?? node.completedAt;
41
41
  node.output = latest.snapshot;
42
- if (status === "failed")
42
+ if (status === "failed" || status === "timed_out")
43
43
  node.error = stringValue(latest.snapshot.summary) ?? node.error;
44
44
  }
45
45
  }
@@ -67,7 +67,7 @@ function createAsyncAgentRunNode(parent, piboSessionId, startedAt, delegation) {
67
67
  title: subagentName,
68
68
  status: asyncAgentStatus(parent, runStatus),
69
69
  startedAt: delegation?.startedAt ?? startedAt ?? parent.startedAt,
70
- completedAt: runStatus === "completed" || runStatus === "cancelled" ? parent.completedAt : undefined,
70
+ completedAt: runStatus === "completed" || runStatus === "cancelled" || runStatus === "failed" || runStatus === "timed_out" ? parent.completedAt : undefined,
71
71
  summary: `Started by ${parent.title}`,
72
72
  input: {
73
73
  startedBy: parent.title,
@@ -5,6 +5,13 @@ import { createRunNotificationNode, parseRunNotificationText } from "./trace-run
5
5
  import { findLikelyTraceChildSession, isSubagentToolName, } from "./trace-subagent-links.js";
6
6
  export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent, childByParent, linkedChildByToolCallId, hasPersistedTranscript, openTranscriptEventIds, sessionStatus) {
7
7
  const payload = storedEvent.payload;
8
+ const confirmedUserMessage = hasPersistedTranscript ? confirmedUserMessageEchoNode(nodes, storedEvent) : undefined;
9
+ if (confirmedUserMessage) {
10
+ if (payload.type === "message_steered" && payload.activeEventId) {
11
+ confirmedUserMessage.parentId = messageTurnNodeId(payload.activeEventId);
12
+ }
13
+ return;
14
+ }
8
15
  if (hasPersistedTranscript &&
9
16
  isTranscriptEchoEvent(payload) &&
10
17
  !shouldKeepTranscriptEchoEvent(payload, openTranscriptEventIds)) {
@@ -84,8 +91,14 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
84
91
  return;
85
92
  }
86
93
  }
87
- if (node.eventId && byId.has(node.id))
88
- return;
94
+ if (node.eventId) {
95
+ const existing = byId.get(node.id);
96
+ if (existing) {
97
+ if (node.type === "user.message" && node.parentId)
98
+ existing.parentId = node.parentId;
99
+ return;
100
+ }
101
+ }
89
102
  attachExecutionCommandToOpenTurn(node, byId);
90
103
  attachAsyncAgentRunNode(node, piboSessionId, storedEvent.createdAt);
91
104
  nodes.push(node);
@@ -174,7 +187,7 @@ export function reconcileTranscriptUserMessageTimestamps(nodes, events) {
174
187
  let userCursor = 0;
175
188
  for (const storedEvent of events) {
176
189
  const event = storedEvent.payload;
177
- if (event.type !== "message_queued" || event.source !== "user")
190
+ if ((event.type !== "message_queued" && event.type !== "message_steered") || event.source !== "user")
178
191
  continue;
179
192
  const eventId = typeof event.eventId === "string" ? event.eventId : storedEvent.eventId;
180
193
  const text = typeof event.text === "string" ? event.text : undefined;
@@ -192,12 +205,15 @@ export function reconcileTranscriptUserMessageTimestamps(nodes, events) {
192
205
  }
193
206
  }
194
207
  export function isConfirmedUserMessageEcho(nodes, event) {
208
+ return Boolean(confirmedUserMessageEchoNode(nodes, event));
209
+ }
210
+ function confirmedUserMessageEchoNode(nodes, event) {
195
211
  const payload = event.payload;
196
- if (payload.type !== "message_queued" || payload.source !== "user")
197
- return false;
212
+ if ((payload.type !== "message_queued" && payload.type !== "message_steered") || payload.source !== "user")
213
+ return undefined;
198
214
  const eventId = typeof payload.eventId === "string" ? payload.eventId : event.eventId;
199
215
  const text = typeof payload.text === "string" ? payload.text : undefined;
200
- return nodes.some((node) => {
216
+ return flattenTraceNodes([...nodes]).find((node) => {
201
217
  if (node.type !== "user.message" || node.source !== "transcript")
202
218
  return false;
203
219
  if (eventId && (node.entryId === eventId || node.stableKey === `entry:${eventId}`))
@@ -261,7 +277,8 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
261
277
  children: [],
262
278
  };
263
279
  switch (event.type) {
264
- case "message_queued": {
280
+ case "message_queued":
281
+ case "message_steered": {
265
282
  const notification = parseRunNotificationText(event.text);
266
283
  if (event.source === "service" && notification) {
267
284
  return createRunNotificationNode({
@@ -277,6 +294,7 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
277
294
  }
278
295
  return {
279
296
  ...base,
297
+ ...(event.type === "message_steered" && event.activeEventId ? { parentId: messageTurnNodeId(event.activeEventId) } : {}),
280
298
  type: "user.message",
281
299
  title: "User Message",
282
300
  status: "done",
@@ -368,10 +386,12 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
368
386
  children: [],
369
387
  };
370
388
  }
371
- case "subagent_session":
389
+ case "subagent_session": {
390
+ const childPiboSessionId = nonEmptyString(event.childPiboSessionId);
391
+ const eventInstanceKey = traceEventInstanceKey(eventSequence, streamId, event);
372
392
  return {
373
393
  ...base,
374
- id: event.toolCallId ? `tool:${event.toolCallId}` : id,
394
+ id: event.toolCallId ? `tool:${event.toolCallId}` : `event:subagent_session:${eventInstanceKey}`,
375
395
  eventId,
376
396
  toolCallId: event.toolCallId,
377
397
  type: "agent.delegation",
@@ -379,10 +399,13 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
379
399
  status: sessionStatus === "running" ? "running" : "done",
380
400
  summary: event.subagentName,
381
401
  input: { subagentName: event.subagentName, threadKey: event.threadKey },
382
- linkedPiboSessionId: event.childPiboSessionId,
383
- stableKey: event.toolCallId ? `tool:${event.toolCallId}` : `subagent:${event.childPiboSessionId}`,
402
+ linkedPiboSessionId: childPiboSessionId,
403
+ stableKey: event.toolCallId
404
+ ? `tool:${event.toolCallId}`
405
+ : childPiboSessionId ? `subagent:${childPiboSessionId}` : `subagent:event:${eventInstanceKey}`,
384
406
  children: [],
385
407
  };
408
+ }
386
409
  case "execution_result":
387
410
  if (isInternalSessionOperation(event.action))
388
411
  return undefined;
@@ -394,21 +417,24 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
394
417
  input: { action: event.action },
395
418
  output: event.result,
396
419
  };
397
- case "compaction_start":
420
+ case "compaction_start": {
421
+ const eventInstanceKey = traceEventInstanceKey(eventSequence, streamId, event);
398
422
  return {
399
423
  ...base,
400
- id: `event:compaction:${eventSequence ?? streamId ?? cryptoSafeId(event)}`,
424
+ id: `event:compaction:${eventInstanceKey}`,
401
425
  type: "execution.compaction",
402
426
  title: "compact",
403
427
  status: "running",
404
428
  summary: "Compacting",
405
429
  input: { reason: event.reason },
406
- stableKey: "compaction:active",
430
+ stableKey: `compaction:${eventInstanceKey}`,
407
431
  };
408
- case "compaction_end":
432
+ }
433
+ case "compaction_end": {
434
+ const eventInstanceKey = traceEventInstanceKey(eventSequence, streamId, event);
409
435
  return {
410
436
  ...base,
411
- id: `event:compaction:end:${eventSequence ?? streamId ?? cryptoSafeId(event)}`,
437
+ id: `event:compaction:end:${eventInstanceKey}`,
412
438
  type: "execution.compaction",
413
439
  title: "compact",
414
440
  status: event.errorMessage ? "error" : "done",
@@ -417,8 +443,9 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
417
443
  input: { reason: event.reason },
418
444
  output: event.result,
419
445
  error: event.errorMessage,
420
- stableKey: "compaction:active",
446
+ stableKey: `compaction:${eventInstanceKey}`,
421
447
  };
448
+ }
422
449
  case "session_error":
423
450
  return {
424
451
  ...base,
@@ -435,7 +462,7 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
435
462
  }
436
463
  }
437
464
  function mergeAssistantDeltaEvent(nodes, byId, event, sessionStatus, createdAt, eventSequence, streamId, streamFrameIndex) {
438
- if (event.text.length === 0)
465
+ if (typeof event.text !== "string" || event.text.length === 0)
439
466
  return;
440
467
  const assistantId = assistantEventNodeId(event);
441
468
  const id = assistantId ? assistantMessageNodeId(assistantId) : `event:assistant_delta:${cryptoSafeId(event)}`;
@@ -474,7 +501,7 @@ function mergeAssistantMessageEvent(target, update) {
474
501
  target.completedAt = update.completedAt ?? target.completedAt;
475
502
  }
476
503
  function mergeThinkingDeltaEvent(nodes, byId, event, sessionStatus, createdAt, eventSequence, streamId, streamFrameIndex) {
477
- if (event.text.length === 0)
504
+ if (typeof event.text !== "string" || event.text.length === 0)
478
505
  return;
479
506
  const thinkingId = thinkingEventNodeId(event);
480
507
  const id = thinkingId ? thinkingNodeId(thinkingId) : `event:thinking_delta:${cryptoSafeId(event)}`;
@@ -642,6 +669,7 @@ function eventTraceNodeOrder(eventSequence, type, streamId, streamFrameIndex) {
642
669
  function eventNodeKind(type) {
643
670
  switch (type) {
644
671
  case "message_queued":
672
+ case "message_steered":
645
673
  return "user.message";
646
674
  case "message_started":
647
675
  case "message_finished":
@@ -792,6 +820,18 @@ function parseTimestamp(value) {
792
820
  const timestamp = new Date(value).getTime();
793
821
  return Number.isFinite(timestamp) ? timestamp : undefined;
794
822
  }
823
+ function traceEventInstanceKey(eventSequence, streamId, event) {
824
+ if (eventSequence !== undefined)
825
+ return `sequence:${eventSequence}`;
826
+ if (streamId !== undefined)
827
+ return `stream:${streamId}`;
828
+ return cryptoSafeId(event);
829
+ }
830
+ function nonEmptyString(value) {
831
+ if (typeof value !== "string")
832
+ return undefined;
833
+ return value.trim() || undefined;
834
+ }
795
835
  function cryptoSafeId(value) {
796
836
  return base64UrlEncode(new TextEncoder().encode(JSON.stringify(value))).slice(0, 48);
797
837
  }
@@ -22,6 +22,11 @@ export function compareTraceNodes(left, right) {
22
22
  const bySameTurnPhase = compareSameTurnPhase(left, right);
23
23
  if (bySameTurnPhase !== 0)
24
24
  return bySameTurnPhase;
25
+ if (left.orderKey?.sourceRank === right.orderKey?.sourceRank) {
26
+ const bySameSourceOrder = compareTraceOrder(left.orderKey, right.orderKey);
27
+ if (bySameSourceOrder !== 0)
28
+ return bySameSourceOrder;
29
+ }
25
30
  const byStartTime = compareOptionalIsoTime(left.startedAt, right.startedAt);
26
31
  if (byStartTime !== 0)
27
32
  return byStartTime;
@@ -23,6 +23,21 @@ export function mergeOlderTracePage(current, older) {
23
23
  eventLimit: (current.eventLimit ?? 0) + (older.eventLimit ?? older.pageSize ?? 0),
24
24
  };
25
25
  }
26
+ export function mergeRefreshedTracePage(current, refreshed) {
27
+ if (current.piboSessionId !== refreshed.piboSessionId)
28
+ return refreshed;
29
+ return {
30
+ ...refreshed,
31
+ nodes: mergeTraceNodes(current.nodes, refreshed.nodes),
32
+ rawEvents: current.rawEvents.length ? current.rawEvents : refreshed.rawEvents,
33
+ beforeCursor: current.beforeCursor,
34
+ firstEventSequence: current.firstEventSequence ?? refreshed.firstEventSequence,
35
+ nextBeforeSequence: current.nextBeforeSequence,
36
+ nextBeforeCursor: current.nextBeforeCursor,
37
+ hasOlderEvents: current.hasOlderEvents,
38
+ eventLimit: current.eventLimit ?? refreshed.eventLimit,
39
+ };
40
+ }
26
41
  function mergeTraceNodes(olderNodes, currentNodes) {
27
42
  const byId = new Map();
28
43
  for (const node of flattenTraceNodes([...olderNodes])) {
@@ -19,7 +19,7 @@ export function parseRunNotificationText(text) {
19
19
  export function createRunNotificationNode(input) {
20
20
  const runs = runNotificationRuns(input.notification);
21
21
  const singleRun = runs.length === 1 ? runs[0] : undefined;
22
- const failedCount = countRunGroup(input.notification.failed);
22
+ const failedCount = countRunGroup(input.notification.failed) + countRunGroup(input.notification.timedOut);
23
23
  const runningCount = countRunGroup(input.notification.running);
24
24
  return {
25
25
  id: input.id,
@@ -43,6 +43,7 @@ function runNotificationRuns(notification) {
43
43
  return [
44
44
  ...runGroup(notification.completed),
45
45
  ...runGroup(notification.failed),
46
+ ...runGroup(notification.timedOut),
46
47
  ...runGroup(notification.cancelled),
47
48
  ...runGroup(notification.running),
48
49
  ];
@@ -57,6 +58,7 @@ function runNotificationSummary(notification) {
57
58
  const parts = [
58
59
  [countRunGroup(notification.completed), "completed"],
59
60
  [countRunGroup(notification.failed), "failed"],
61
+ [countRunGroup(notification.timedOut), "timed out"],
60
62
  [countRunGroup(notification.cancelled), "cancelled"],
61
63
  [countRunGroup(notification.running), "running"],
62
64
  ]
@@ -12,7 +12,7 @@ function node(input, context) {
12
12
  function runStatus(status) {
13
13
  if (status === "completed")
14
14
  return "done";
15
- if (status === "failed")
15
+ if (status === "failed" || status === "timed_out")
16
16
  return "error";
17
17
  return status;
18
18
  }
@@ -28,7 +28,7 @@ function settleActiveSessionNodes(piboSessionId, context, status, terminalSource
28
28
  }
29
29
  export const sessionLifecycleSignalProducer = {
30
30
  name: "session-lifecycle",
31
- accepts: (input) => ["session_created", "session_disposed", "session_processing_changed", "message_accepted", "queue_changed", "recovery", "session_interrupted", "signal_node_pruned"].includes(input.type),
31
+ accepts: (input) => ["session_created", "session_disposed", "session_processing_changed", "message_accepted", "message_rejected", "queue_changed", "recovery", "session_interrupted", "signal_node_pruned"].includes(input.type),
32
32
  project(input, context) {
33
33
  const data = input;
34
34
  if (data.type === "session_created") {
@@ -78,6 +78,15 @@ export const sessionLifecycleSignalProducer = {
78
78
  }
79
79
  return mutations;
80
80
  }
81
+ if (data.type === "message_rejected") {
82
+ const acceptedTurn = context.getNode(`turn:${data.piboSessionId}:${data.eventId}`);
83
+ return [
84
+ { type: "remove_node", nodeId: `message:${data.piboSessionId}:${data.eventId}` },
85
+ ...(acceptedTurn?.metadata?.accepted === true
86
+ ? [{ type: "remove_node", nodeId: acceptedTurn.id }]
87
+ : []),
88
+ ];
89
+ }
81
90
  if (data.type === "session_disposed") {
82
91
  return [
83
92
  ...settleActiveSessionNodes(data.piboSessionId, context, "cancelled", "session_disposed"),
@@ -133,6 +142,10 @@ export const outputSignalProducer = {
133
142
  mutations.push({ type: "set_session_queue", piboSessionId, queuedMessages: event.queuedMessages });
134
143
  mutations.push({ type: "upsert_node", node: node({ id: `message:${piboSessionId}:${event.eventId ?? context.now()}`, kind: "message", status: "queued", piboSessionId, metadata: { source: event.source } }, context) });
135
144
  }
145
+ if (event.type === "message_steered") {
146
+ mutations.push({ type: "patch_node", nodeId: `session:${piboSessionId}`, patch: { status: "running" } });
147
+ mutations.push({ type: "upsert_node", node: node({ id: `message:${piboSessionId}:${event.eventId ?? context.now()}`, kind: "message", status: "done", piboSessionId, parentNodeId: event.activeEventId ? `turn:${piboSessionId}:${event.activeEventId}` : undefined, completedAt: context.now(), metadata: { source: event.source, delivery: "steer" } }, context) });
148
+ }
136
149
  if (event.type === "message_started") {
137
150
  mutations.push({ type: "patch_node", nodeId: `session:${piboSessionId}`, patch: { status: "running" } });
138
151
  if (event.eventId) {
@@ -200,7 +213,7 @@ export const runSignalProducer = {
200
213
  if (data.type !== "run_changed")
201
214
  return [];
202
215
  const run = data.run;
203
- return [{ type: "upsert_node", node: node({ id: `run:${run.runId}`, kind: "yielded_run", status: runStatus(run.status), piboSessionId: run.controllerPiboSessionId, startedAt: run.createdAt, completedAt: run.completedAt, error: run.status === "failed" ? { message: run.summary ?? "Run failed.", source: "run" } : undefined, metadata: { runId: run.runId, toolName: run.toolName, completionPolicy: run.completionPolicy, consumed: run.consumed, summary: run.summary, previousStatus: data.previousStatus, reason: data.reason } }, context) }];
216
+ return [{ type: "upsert_node", node: node({ id: `run:${run.runId}`, kind: "yielded_run", status: runStatus(run.status), piboSessionId: run.controllerPiboSessionId, startedAt: run.createdAt, completedAt: run.completedAt, error: run.status === "failed" || run.status === "timed_out" ? { message: run.summary ?? "Run failed.", source: "run" } : undefined, metadata: { runId: run.runId, toolName: run.toolName, completionPolicy: run.completionPolicy, consumed: run.consumed, summary: run.summary, timeoutMs: run.timeoutMs, timeoutAt: run.timeoutAt, timeoutPhase: run.timeoutPhase, serviceWarning: run.serviceWarning, previousStatus: data.previousStatus, reason: data.reason } }, context) }];
204
217
  },
205
218
  };
206
219
  export function createDefaultSignalProducers() {