@opengeni/react 1.0.1 → 2.3.0-canary.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 (174) hide show
  1. package/README.md +33 -10
  2. package/dist/artifacts-spreadsheet.js +1 -1
  3. package/dist/artifacts.js +1 -1
  4. package/dist/browser-viewer-GXUICIAG.js +14 -0
  5. package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
  6. package/dist/chunk-4MDLEHXL.js.map +1 -0
  7. package/dist/chunk-5YK2ZDIG.js +116 -0
  8. package/dist/chunk-5YK2ZDIG.js.map +1 -0
  9. package/dist/chunk-H5YGYNXS.js +329 -0
  10. package/dist/chunk-H5YGYNXS.js.map +1 -0
  11. package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
  12. package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
  13. package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
  14. package/dist/chunk-JOAVA2TW.js.map +1 -0
  15. package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
  16. package/dist/chunk-LS4POUTY.js.map +1 -0
  17. package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
  18. package/dist/chunk-LTCHJ4MS.js.map +1 -0
  19. package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
  20. package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
  21. package/dist/chunk-QGBJ6GHG.js +2300 -0
  22. package/dist/chunk-QGBJ6GHG.js.map +1 -0
  23. package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
  24. package/dist/chunk-RSXJN73N.js.map +1 -0
  25. package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
  26. package/dist/chunk-SE4W4WQ3.js.map +1 -0
  27. package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
  28. package/dist/chunk-W6NFJXSA.js.map +1 -0
  29. package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
  30. package/dist/chunk-X4A66EWZ.js.map +1 -0
  31. package/dist/chunk-X4JAFD5R.js +3907 -0
  32. package/dist/chunk-X4JAFD5R.js.map +1 -0
  33. package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
  34. package/dist/chunk-XXWH2JLC.js.map +1 -0
  35. package/dist/client.d.ts +2 -2
  36. package/dist/components/browser-viewer.d.ts +7 -1
  37. package/dist/components/computer-viewer.d.ts +4 -1
  38. package/dist/components/enrollment-consent.d.ts +6 -3
  39. package/dist/components/file-browser.d.ts +7 -1
  40. package/dist/components/human-input-form.d.ts +1 -1
  41. package/dist/components/human-input-surface.d.ts +1 -1
  42. package/dist/components/machine-card.d.ts +4 -1
  43. package/dist/components/machines/machine-detail.d.ts +4 -1
  44. package/dist/components/machines/machine-operation-policy.d.ts +11 -0
  45. package/dist/components/machines-dashboard.d.ts +3 -1
  46. package/dist/components/markdown.d.ts +20 -1
  47. package/dist/components/message-timeline.d.ts +1 -1
  48. package/dist/components/model-policy-picker.d.ts +2 -0
  49. package/dist/components/pierre-file.d.ts +1 -1
  50. package/dist/components/sandbox-files.d.ts +8 -2
  51. package/dist/components/sandbox-workspace.d.ts +33 -3
  52. package/dist/components/session-chrome.d.ts +15 -3
  53. package/dist/components/workspace-dock.d.ts +6 -2
  54. package/dist/composer.js +5 -5
  55. package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
  56. package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
  57. package/dist/hooks/channel-cache.d.ts +23 -0
  58. package/dist/hooks/shared-machines-list.d.ts +14 -0
  59. package/dist/hooks/use-attached-browsers.d.ts +2 -1
  60. package/dist/hooks/use-channels.d.ts +1 -0
  61. package/dist/hooks/use-composer.d.ts +37 -9
  62. package/dist/hooks/use-machine-chip.d.ts +3 -3
  63. package/dist/hooks/use-machines.d.ts +19 -1
  64. package/dist/hooks/use-sandbox-files.d.ts +1 -0
  65. package/dist/hooks/use-session-background-commands.d.ts +14 -0
  66. package/dist/hooks/use-turn-queue.d.ts +12 -0
  67. package/dist/hooks/use-workspace-sessions.d.ts +1 -0
  68. package/dist/index.d.ts +7 -5
  69. package/dist/index.js +1096 -623
  70. package/dist/index.js.map +1 -1
  71. package/dist/interaction.js +29 -181
  72. package/dist/interaction.js.map +1 -1
  73. package/dist/lib/interaction-errors.d.ts +3 -0
  74. package/dist/machines.d.ts +4 -2
  75. package/dist/machines.js +10 -2
  76. package/dist/model-policy.d.ts +1 -1
  77. package/dist/model-policy.js +1 -1
  78. package/dist/platform-activity-row-BJHUHXAT.js +169 -0
  79. package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
  80. package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
  81. package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
  82. package/dist/realtime/realtime-control.d.ts +6 -3
  83. package/dist/realtime.js +36 -14
  84. package/dist/realtime.js.map +1 -1
  85. package/dist/session-ui.d.ts +1 -1
  86. package/dist/session-ui.js +6 -2
  87. package/dist/session.d.ts +2 -2
  88. package/dist/session.js +4 -4
  89. package/dist/timeline/index.d.ts +1 -1
  90. package/dist/timeline/platform-activity-row.d.ts +15 -0
  91. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  92. package/dist/timeline/shared.d.ts +4 -2
  93. package/dist/timeline/types.d.ts +50 -5
  94. package/dist/types/machines.d.ts +2 -2
  95. package/package.json +3 -3
  96. package/src/client.ts +12 -1
  97. package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
  98. package/src/components/browser-viewer.tsx +361 -58
  99. package/src/components/chat-composer.tsx +1 -1
  100. package/src/components/composer.tsx +6 -6
  101. package/src/components/computer-viewer.tsx +368 -115
  102. package/src/components/enrollment-consent.tsx +64 -6
  103. package/src/components/file-browser.tsx +117 -2
  104. package/src/components/human-input-form.tsx +103 -59
  105. package/src/components/human-input-surface.tsx +1 -1
  106. package/src/components/machine-card.tsx +111 -0
  107. package/src/components/machine-input-display.ts +15 -0
  108. package/src/components/machines/machine-detail.tsx +47 -0
  109. package/src/components/machines/machine-operation-policy.tsx +179 -0
  110. package/src/components/machines-dashboard.tsx +6 -0
  111. package/src/components/markdown.tsx +174 -20
  112. package/src/components/message-timeline.tsx +215 -18
  113. package/src/components/model-policy-picker.tsx +9 -17
  114. package/src/components/pierre-file.tsx +1 -1
  115. package/src/components/queue-surface-implementation.tsx +82 -16
  116. package/src/components/sandbox-files.tsx +135 -19
  117. package/src/components/sandbox-workspace.tsx +204 -92
  118. package/src/components/session-chrome.tsx +415 -208
  119. package/src/components/session-status.tsx +1 -1
  120. package/src/components/user-message-body.tsx +168 -48
  121. package/src/components/workspace-dock.tsx +141 -175
  122. package/src/hooks/channel-cache.ts +104 -0
  123. package/src/hooks/shared-machines-list.ts +239 -0
  124. package/src/hooks/use-attached-browsers.ts +5 -1
  125. package/src/hooks/use-browser-frame-stream.ts +6 -1
  126. package/src/hooks/use-browser-session.ts +6 -4
  127. package/src/hooks/use-browser-sessions.ts +7 -1
  128. package/src/hooks/use-channels.ts +32 -10
  129. package/src/hooks/use-composer.ts +760 -291
  130. package/src/hooks/use-computer-frame-stream.ts +14 -9
  131. package/src/hooks/use-computer-session.ts +16 -14
  132. package/src/hooks/use-computer-sessions.ts +8 -1
  133. package/src/hooks/use-last-started-turn-policy.ts +1 -1
  134. package/src/hooks/use-machine-chip.ts +23 -6
  135. package/src/hooks/use-machines.ts +104 -6
  136. package/src/hooks/use-sandbox-files.ts +195 -140
  137. package/src/hooks/use-sandbox-git.ts +58 -29
  138. package/src/hooks/use-session-background-commands.ts +56 -0
  139. package/src/hooks/use-session-capabilities.ts +18 -0
  140. package/src/hooks/use-turn-queue.ts +111 -12
  141. package/src/hooks/use-workspace-sessions.ts +5 -1
  142. package/src/index.ts +26 -4
  143. package/src/lib/interaction-errors.ts +18 -0
  144. package/src/machines.ts +9 -1
  145. package/src/model-policy.ts +10 -2
  146. package/src/realtime/realtime-control.tsx +53 -17
  147. package/src/session-context.ts +1 -0
  148. package/src/session-ui.ts +6 -1
  149. package/src/session.ts +9 -1
  150. package/src/timeline/activity-rail.tsx +20 -75
  151. package/src/timeline/index.ts +4 -0
  152. package/src/timeline/platform-activity-row.tsx +201 -0
  153. package/src/timeline/projection.ts +529 -25
  154. package/src/timeline/screenshot-lightbox.tsx +48 -14
  155. package/src/timeline/shared.tsx +8 -2
  156. package/src/timeline/tool-renderers.tsx +104 -6
  157. package/src/timeline/turn-summary.tsx +6 -1
  158. package/src/timeline/types.ts +77 -2
  159. package/src/types/machines.ts +10 -0
  160. package/styles/compiled.css +115 -104
  161. package/dist/chunk-5UQPNLIO.js.map +0 -1
  162. package/dist/chunk-6KLX2RH5.js.map +0 -1
  163. package/dist/chunk-B6WPZSTT.js.map +0 -1
  164. package/dist/chunk-HA46CYQK.js.map +0 -1
  165. package/dist/chunk-IGC6A2ZI.js.map +0 -1
  166. package/dist/chunk-LLOLUHVG.js +0 -5937
  167. package/dist/chunk-LLOLUHVG.js.map +0 -1
  168. package/dist/chunk-MKPIAGVL.js.map +0 -1
  169. package/dist/chunk-R3AZOAB5.js.map +0 -1
  170. package/dist/chunk-XBZWOM2K.js.map +0 -1
  171. package/dist/chunk-Y7OEDUJ5.js.map +0 -1
  172. package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
  173. package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
  174. /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
@@ -1,5 +1,8 @@
1
1
  import {
2
2
  parseMediaGenerationResult,
3
+ type HumanInputAnswer,
4
+ type HumanInputQuestion,
5
+ type HumanInputResponse,
3
6
  type SessionEvent,
4
7
  type SessionStatus,
5
8
  type TimelineAnnotation,
@@ -18,9 +21,13 @@ import type {
18
21
  AuthNeededItem,
19
22
  ContextCompactionItem,
20
23
  GoalItem,
24
+ HumanInputAnswerSummary,
25
+ HumanInputItem,
21
26
  MachineInputBatchItem,
22
27
  MemoryItem,
23
28
  SandboxItem,
29
+ StartupPhase,
30
+ StartupPhaseItem,
24
31
  SessionStatusItem,
25
32
  TimelineGroup,
26
33
  TimelineItem,
@@ -124,6 +131,92 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
124
131
  const items: TimelineItem[] = [];
125
132
  const prescan = prescanTurnAnchors(events);
126
133
  const ordered = orderTimelineEvents(events, prescan);
134
+ const humanInputRequests = humanInputRequestsById(events);
135
+ const humanInputToolCallIds = new Set(
136
+ [...humanInputRequests.values()]
137
+ .map((request) => stringValue(request.toolCallId))
138
+ .filter(Boolean),
139
+ );
140
+ const queuedAtByTurn = new Map<string, string>();
141
+ const startupRecoveryRevisionByTurn = new Map<string, number>();
142
+ const startupPhases = new Map<string, readonly [StartupPhaseItem, string | null, number]>();
143
+ let startupEvent: SessionEvent;
144
+ let startupTurnId: string | null = null;
145
+ let startupAttemptId: string | null = null;
146
+ let startupRecoveryRevision = 0;
147
+
148
+ const settleStartupPhase = (
149
+ phase: StartupPhase,
150
+ status: StartupPhaseItem["status"],
151
+ duration: number | null,
152
+ outcome: StartupPhaseItem["outcome"],
153
+ mode = 0,
154
+ explicitStartedAt?: string,
155
+ explicitId?: string,
156
+ ): void => {
157
+ const key = `${startupTurnId}:${phase}`;
158
+ const priorState = startupPhases.get(key);
159
+ const prior = priorState?.[0];
160
+ const comparedByAttempt = priorState?.[1] && startupAttemptId;
161
+ const sameAttempt = comparedByAttempt
162
+ ? priorState[1] === startupAttemptId
163
+ : (priorState?.[2] ?? 0) === startupRecoveryRevision;
164
+ const open = prior?.status === "running" && sameAttempt ? prior : undefined;
165
+ const replacesPriorAttempt =
166
+ prior !== undefined &&
167
+ (comparedByAttempt ? !sameAttempt : startupRecoveryRevision > (priorState?.[2] ?? 0));
168
+ // A later activity attempt may replay preparation that the logical turn had
169
+ // already completed before the worker moved. Keep that first successful
170
+ // landmark; only an abandoned or failed attempt needs replacement.
171
+ if (replacesPriorAttempt && prior.status === "complete") return;
172
+ if (mode === 2 && prior && !replacesPriorAttempt) return;
173
+ if (mode === 1 && !open) return;
174
+ if (!replacesPriorAttempt && status !== "running" && open) {
175
+ open.status = status;
176
+ open.completedAt = startupEvent.occurredAt;
177
+ open.durationMs =
178
+ duration === null
179
+ ? elapsedDurationMs(open.startedAt, startupEvent.occurredAt)
180
+ : Math.max(0, duration);
181
+ open.outcome = outcome ?? open.outcome;
182
+ return;
183
+ }
184
+ const running = status === "running";
185
+ const durationMs = running || duration === null ? null : Math.max(0, duration);
186
+ const completedEpoch = Date.parse(startupEvent.occurredAt);
187
+ const startedAt =
188
+ explicitStartedAt ??
189
+ (!running && durationMs !== null && Number.isFinite(completedEpoch)
190
+ ? new Date(completedEpoch - durationMs).toISOString()
191
+ : startupEvent.occurredAt);
192
+ if (replacesPriorAttempt) {
193
+ Object.assign(prior, {
194
+ id: explicitId ?? startupEvent.id,
195
+ status,
196
+ startedAt,
197
+ completedAt: running ? null : startupEvent.occurredAt,
198
+ durationMs,
199
+ outcome,
200
+ occurredAt: startedAt,
201
+ } satisfies Partial<StartupPhaseItem>);
202
+ startupPhases.set(key, [prior, startupAttemptId, startupRecoveryRevision]);
203
+ return;
204
+ }
205
+ const item: StartupPhaseItem = {
206
+ kind: "startup-phase",
207
+ id: explicitId ?? startupEvent.id,
208
+ turnId: startupTurnId,
209
+ phase,
210
+ status,
211
+ startedAt,
212
+ completedAt: running ? null : startupEvent.occurredAt,
213
+ durationMs,
214
+ outcome,
215
+ occurredAt: startedAt,
216
+ };
217
+ items.push(item);
218
+ startupPhases.set(key, [item, startupAttemptId, startupRecoveryRevision]);
219
+ };
127
220
 
128
221
  const last = (): TimelineItem | undefined => items[items.length - 1];
129
222
 
@@ -138,6 +231,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
138
231
  const finalizeOpen = (
139
232
  turnId?: string | null,
140
233
  disposition: "complete" | "failed" | "cancelled" = "complete",
234
+ completedAt?: string,
141
235
  ): void => {
142
236
  for (const item of items) {
143
237
  if (
@@ -158,12 +252,47 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
158
252
  if (item.kind === "sandbox" && item.status === "running") {
159
253
  item.status = disposition;
160
254
  }
255
+ if (item.kind === "startup-phase" && item.status === "running") {
256
+ item.status = disposition;
257
+ if (completedAt) {
258
+ item.completedAt = completedAt;
259
+ item.durationMs = elapsedDurationMs(item.startedAt, completedAt);
260
+ }
261
+ }
161
262
  }
162
263
  };
163
264
 
164
265
  for (const event of ordered) {
165
266
  const payload = asRecord(event.payload);
166
267
  const turnId = event.turnId ?? null;
268
+ startupEvent = event;
269
+ startupTurnId = turnId;
270
+ startupAttemptId =
271
+ (typeof event.turnAttemptId === "string" ? event.turnAttemptId : null) ??
272
+ stringValue(payload.attemptId);
273
+ startupRecoveryRevision = turnId ? (startupRecoveryRevisionByTurn.get(turnId) ?? 0) : 0;
274
+ const setupPhase = event.type.startsWith("rig.setup.")
275
+ ? "rig"
276
+ : event.type.startsWith("turn.startup.phase.")
277
+ ? startupPhaseFromPayload(payload.phase)
278
+ : null;
279
+ const setupStatus = event.type.endsWith(".started")
280
+ ? "running"
281
+ : event.type.endsWith(".failed")
282
+ ? "failed"
283
+ : event.type.endsWith(".completed") || event.type === "rig.setup.skipped"
284
+ ? "complete"
285
+ : null;
286
+ if (setupPhase && setupStatus) {
287
+ closeStreamingTail();
288
+ settleStartupPhase(
289
+ setupPhase,
290
+ setupStatus,
291
+ numberOrNull(payload.durationMs),
292
+ event.type === "rig.setup.skipped" ? "skipped" : null,
293
+ );
294
+ continue;
295
+ }
167
296
 
168
297
  switch (event.type) {
169
298
  case "user.message": {
@@ -191,6 +320,9 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
191
320
  items.push({
192
321
  kind: "user-message",
193
322
  id: event.id,
323
+ ...(event.clientEventId
324
+ ? { reconciliationKey: `user-message:${event.clientEventId}` }
325
+ : {}),
194
326
  text: voiceMessage?.text ?? stringValue(payload.text),
195
327
  annotations: timelineAnnotations(payload.annotations),
196
328
  annotationSource: {
@@ -209,6 +341,18 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
209
341
  break;
210
342
  }
211
343
 
344
+ case "user.humanInputResponse": {
345
+ // A structured answer resumes the frozen tool call rather than adding a
346
+ // synthetic user.message to model history. Keep the original question
347
+ // and the resolved human choice together as first-class chat history;
348
+ // neither belongs behind the generic activity fold.
349
+ const item = humanInputItemFromResponse(event, payload, humanInputRequests);
350
+ if (!item) break;
351
+ closeStreamingTail();
352
+ items.push(item);
353
+ break;
354
+ }
355
+
212
356
  case "system.update.delivered": {
213
357
  const inputs = machineInputMembers(payload.members);
214
358
  if (inputs.length === 0) break;
@@ -253,6 +397,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
253
397
 
254
398
  case "agent.message.completed": {
255
399
  const text = stringValue(payload.text);
400
+ const phase = assistantMessagePhase(payload.phase);
256
401
  // Reconcile the most recent same-turn agent message — even when
257
402
  // activity (tool calls, reasoning) landed after its deltas — so the
258
403
  // completed text never duplicates the streamed one.
@@ -276,6 +421,9 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
276
421
  open.text = text || open.text;
277
422
  }
278
423
  open.streaming = false;
424
+ // A phase-less settlement mirror (the worker's final-output receipt)
425
+ // must not erase the provider-declared phase captured by the SDK item.
426
+ if (phase) open.phase = phase;
279
427
  // Completion time is what the footer shows ("finished at"); keep the
280
428
  // first-delta stamp only until this event arrives.
281
429
  open.occurredAt = event.occurredAt;
@@ -307,6 +455,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
307
455
  id: event.id,
308
456
  turnId,
309
457
  text,
458
+ ...(phase ? { phase } : {}),
310
459
  streaming: false,
311
460
  occurredAt: event.occurredAt,
312
461
  annotationSource: {
@@ -349,6 +498,12 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
349
498
  const callId = typeof payload.id === "string" ? payload.id : null;
350
499
  const args = payload.arguments ?? null;
351
500
  closeStreamingTail();
501
+ if (callId && humanInputToolCallIds.has(callId)) {
502
+ // The matching session.humanInput.requested / response lifecycle owns
503
+ // the visible chat card. Rendering the raw tool too would hide the
504
+ // decision behind a duplicate generic "Ask" step.
505
+ break;
506
+ }
352
507
  if (
353
508
  toolMatchesLeaf(name, WORKER_SPAWN_TOOL) ||
354
509
  toolMatchesLeaf(name, WORKER_MESSAGE_TOOL)
@@ -452,15 +607,58 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
452
607
  break;
453
608
  }
454
609
 
610
+ case "turn.queued": {
611
+ const queuedTurnId = turnId ?? (typeof payload.turnId === "string" ? payload.turnId : null);
612
+ if (queuedTurnId) {
613
+ queuedAtByTurn.set(queuedTurnId, event.occurredAt);
614
+ }
615
+ break;
616
+ }
617
+
618
+ case "turn.started": {
619
+ if (!turnId) break;
620
+ const queuedAt = queuedAtByTurn.get(turnId);
621
+ if (!queuedAt) break;
622
+ // Recovery starts the same logical turn again. Queue latency belongs to
623
+ // the first admission only, so consume the queued timestamp after its
624
+ // first projection instead of manufacturing another queue span.
625
+ queuedAtByTurn.delete(turnId);
626
+ closeStreamingTail();
627
+ settleStartupPhase(
628
+ "queue",
629
+ "complete",
630
+ elapsedDurationMs(queuedAt, event.occurredAt),
631
+ null,
632
+ 0,
633
+ queuedAt,
634
+ `${event.id}-queue`,
635
+ );
636
+ break;
637
+ }
638
+
455
639
  case "sandbox.operation.started":
456
640
  case "sandbox.operation.completed":
457
641
  case "sandbox.operation.failed": {
458
642
  const name = typeof payload.name === "string" ? payload.name : "sandbox";
459
643
  const status = event.type.endsWith(".failed")
460
- ? "failed"
644
+ ? payload.expectedTransition === true
645
+ ? "cancelled"
646
+ : "failed"
461
647
  : event.type.endsWith(".completed")
462
648
  ? "complete"
463
649
  : "running";
650
+ const origin =
651
+ payload.origin === "created" ||
652
+ payload.origin === "restored" ||
653
+ payload.origin === "resumed"
654
+ ? payload.origin
655
+ : null;
656
+ const startupPhase = startupPhaseForSandboxOperation(name);
657
+ if (startupPhase) {
658
+ closeStreamingTail();
659
+ settleStartupPhase(startupPhase, status, numberOrNull(payload.durationMs), origin);
660
+ break;
661
+ }
464
662
  // Routine per-turn platform plumbing that runs before EVERY turn to
465
663
  // guarantee box contents survive a re-warm — NOT the agent redoing work:
466
664
  // - repository-clone: idempotent clone check + off-manifest token re-seed;
@@ -480,13 +678,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
480
678
  const existing = findOpenSandbox(items, name);
481
679
  if (existing && status !== "running") {
482
680
  existing.status = status;
483
- if (
484
- payload.origin === "created" ||
485
- payload.origin === "restored" ||
486
- payload.origin === "resumed"
487
- ) {
488
- existing.origin = payload.origin;
489
- }
681
+ if (origin) existing.origin = origin;
490
682
  const message = failureMessage(payload);
491
683
  if (message) {
492
684
  existing.output = existing.output ? `${existing.output}\n${message}` : message;
@@ -502,12 +694,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
502
694
  name,
503
695
  command: typeof payload.command === "string" ? payload.command : null,
504
696
  output: failureMessage(payload) ?? "",
505
- origin:
506
- payload.origin === "created" ||
507
- payload.origin === "restored" ||
508
- payload.origin === "resumed"
509
- ? payload.origin
510
- : null,
697
+ origin,
511
698
  status,
512
699
  occurredAt: event.occurredAt,
513
700
  });
@@ -515,6 +702,29 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
515
702
  break;
516
703
  }
517
704
 
705
+ case "agent.model.request": {
706
+ const requestPhase = typeof payload.phase === "string" ? payload.phase : null;
707
+ const status =
708
+ requestPhase === "started"
709
+ ? "running"
710
+ : requestPhase === "first_byte" || requestPhase === "first_event"
711
+ ? "complete"
712
+ : requestPhase === "failed" || requestPhase === "timed_out"
713
+ ? "failed"
714
+ : null;
715
+ if (status) {
716
+ if (status === "running") closeStreamingTail();
717
+ settleStartupPhase(
718
+ "provider_first_byte",
719
+ status,
720
+ status === "running" ? null : numberOrNull(payload.durationMs),
721
+ null,
722
+ status === "running" ? 2 : 1,
723
+ );
724
+ }
725
+ break;
726
+ }
727
+
518
728
  case "sandbox.command.output.delta": {
519
729
  // `chunk` is the canonical wire field; text/output are legacy shapes.
520
730
  const text =
@@ -572,7 +782,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
572
782
  }
573
783
 
574
784
  case "session.requiresAction": {
575
- finalizeOpen(turnId);
785
+ finalizeOpen(turnId, "complete", event.occurredAt);
576
786
  items.push({
577
787
  kind: "notice",
578
788
  id: event.id,
@@ -639,6 +849,12 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
639
849
  // Recovery requests are durable control-plane evidence, not a user
640
850
  // message or proof that recovery succeeded. Live state already exposes
641
851
  // a genuinely recovering session; keep the raw event in Debug/audit.
852
+ if (turnId) {
853
+ startupRecoveryRevisionByTurn.set(
854
+ turnId,
855
+ (startupRecoveryRevisionByTurn.get(turnId) ?? 0) + 1,
856
+ );
857
+ }
642
858
  break;
643
859
  }
644
860
 
@@ -700,7 +916,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
700
916
  // adding a generic turn chip would falsely make it look like an
701
917
  // extra agent response.
702
918
  if (payload.maintenance === "context_compaction") {
703
- finalizeOpen(turnId);
919
+ finalizeOpen(turnId, "complete", event.occurredAt);
704
920
  break;
705
921
  }
706
922
  // Credit exhaustion arrives as a NOMINALLY completed turn (`detail:
@@ -710,7 +926,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
710
926
  // session looking healthy while every future turn silently dies, so it
711
927
  // projects exactly like a failed turn plus an explicit notice.
712
928
  if (isCreditExhaustionPayload(payload)) {
713
- finalizeOpen(turnId);
929
+ finalizeOpen(turnId, "complete", event.occurredAt);
714
930
  items.push(turnEndItem(event, "failed", CREDIT_EXHAUSTION_MESSAGE));
715
931
  items.push({
716
932
  kind: "notice",
@@ -721,7 +937,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
721
937
  });
722
938
  break;
723
939
  }
724
- finalizeOpen(turnId);
940
+ finalizeOpen(turnId, "complete", event.occurredAt);
725
941
  items.push(turnEndItem(event, "complete", null));
726
942
  break;
727
943
  }
@@ -738,7 +954,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
738
954
  // spent once, on the turn-level outcome. Items caught mid-flight read
739
955
  // as calm "interrupted" (same as turn.cancelled); an item that itself
740
956
  // failed keeps its own failed status from its output event.
741
- finalizeOpen(turnId, "cancelled");
957
+ finalizeOpen(turnId, "cancelled", event.occurredAt);
742
958
  items.push(turnEndItem(event, "failed", failureText));
743
959
  if (!hadActivity) {
744
960
  items.push({
@@ -760,7 +976,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
760
976
  break;
761
977
  }
762
978
  const hadActivity = hasTurnActivity(items, turnId);
763
- finalizeOpen(turnId, "cancelled");
979
+ finalizeOpen(turnId, "cancelled", event.occurredAt);
764
980
  items.push(turnEndItem(event, "cancelled", null));
765
981
  if (!hadActivity) {
766
982
  items.push({
@@ -802,6 +1018,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
802
1018
  case "goal.paused":
803
1019
  case "goal.resumed":
804
1020
  case "goal.cleared":
1021
+ case "goal.held":
805
1022
  case "goal.continuation": {
806
1023
  // Agent tool mutations already appear as tool-call rows in the activity
807
1024
  // cluster. Re-emitting them as GoalRow landmarks splits "N steps" mid-turn.
@@ -812,7 +1029,10 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
812
1029
  kind: "goal",
813
1030
  id: event.id,
814
1031
  action: event.type.slice("goal.".length) as GoalItem["action"],
815
- text: goalText(payload),
1032
+ text:
1033
+ event.type === "goal.held" && typeof payload.reason === "string" && payload.reason
1034
+ ? payload.reason
1035
+ : goalText(payload),
816
1036
  occurredAt: event.occurredAt,
817
1037
  });
818
1038
  break;
@@ -849,6 +1069,134 @@ function realtimeVoiceMessage(payload: Record<string, unknown>): {
849
1069
  return null;
850
1070
  }
851
1071
 
1072
+ function humanInputRequestsById(events: SessionEvent[]): Map<string, Record<string, unknown>> {
1073
+ const requests = new Map<string, Record<string, unknown>>();
1074
+ for (const event of events) {
1075
+ if (event.type !== "session.humanInput.requested") continue;
1076
+ const request = asRecord(asRecord(event.payload).request);
1077
+ const requestId = stringValue(request.id);
1078
+ if (requestId) requests.set(requestId, request);
1079
+ }
1080
+ return requests;
1081
+ }
1082
+
1083
+ function humanInputItemFromResponse(
1084
+ event: SessionEvent,
1085
+ payload: Record<string, unknown>,
1086
+ requests: Map<string, Record<string, unknown>>,
1087
+ ): HumanInputItem | null {
1088
+ const response = asRecord(payload.response);
1089
+ const outcome = response.outcome;
1090
+ if (
1091
+ outcome !== "answered" &&
1092
+ outcome !== "skipped" &&
1093
+ outcome !== "expired" &&
1094
+ outcome !== "cancelled"
1095
+ ) {
1096
+ return null;
1097
+ }
1098
+
1099
+ const requestId = stringValue(payload.requestId);
1100
+ const request = requests.get(requestId);
1101
+ const questions = humanInputQuestions(request?.questions);
1102
+ const normalizedResponse: HumanInputResponse =
1103
+ outcome === "answered"
1104
+ ? {
1105
+ outcome,
1106
+ answers: Array.isArray(response.answers)
1107
+ ? response.answers
1108
+ .map(asRecord)
1109
+ .map(humanInputAnswer)
1110
+ .filter((answer): answer is HumanInputAnswer => answer !== null)
1111
+ : [],
1112
+ }
1113
+ : { outcome };
1114
+ const questionsById = new Map(questions.map((question) => [question.id, question]));
1115
+ const answers =
1116
+ normalizedResponse.outcome === "answered"
1117
+ ? normalizedResponse.answers.map((answer) =>
1118
+ humanInputAnswerSummary(answer, questionsById.get(answer.questionId)),
1119
+ )
1120
+ : [];
1121
+ return {
1122
+ kind: "human-input",
1123
+ id: event.id,
1124
+ turnId: event.turnId ?? null,
1125
+ requestId,
1126
+ questions,
1127
+ response: normalizedResponse,
1128
+ answers,
1129
+ occurredAt: event.occurredAt,
1130
+ };
1131
+ }
1132
+
1133
+ function humanInputQuestions(value: unknown): HumanInputQuestion[] {
1134
+ if (!Array.isArray(value)) return [];
1135
+ return value.map(asRecord).flatMap((question) => {
1136
+ const id = stringValue(question.id);
1137
+ const prompt = stringValue(question.prompt);
1138
+ const kind = question.kind;
1139
+ if (
1140
+ !id ||
1141
+ !prompt ||
1142
+ (kind !== "text" && kind !== "single_select" && kind !== "multi_select")
1143
+ ) {
1144
+ return [];
1145
+ }
1146
+ const options = Array.isArray(question.options)
1147
+ ? question.options.map(asRecord).flatMap((option) => {
1148
+ const optionId = stringValue(option.id);
1149
+ const label = stringValue(option.label);
1150
+ return optionId && label
1151
+ ? [{ id: optionId, label, description: stringValue(option.description) || null }]
1152
+ : [];
1153
+ })
1154
+ : [];
1155
+ return [
1156
+ {
1157
+ id,
1158
+ kind,
1159
+ prompt,
1160
+ label: stringValue(question.label) || null,
1161
+ helpText: stringValue(question.helpText) || null,
1162
+ options,
1163
+ required: question.required === true,
1164
+ allowOther: question.allowOther === true,
1165
+ },
1166
+ ];
1167
+ });
1168
+ }
1169
+
1170
+ function humanInputAnswer(value: Record<string, unknown>): HumanInputAnswer | null {
1171
+ const questionId = stringValue(value.questionId);
1172
+ if (!questionId) return null;
1173
+ return {
1174
+ questionId,
1175
+ values: Array.isArray(value.values)
1176
+ ? value.values.filter((answer): answer is string => typeof answer === "string")
1177
+ : [],
1178
+ ...(typeof value.other === "string" && value.other ? { other: value.other } : {}),
1179
+ };
1180
+ }
1181
+
1182
+ function humanInputAnswerSummary(
1183
+ answer: HumanInputAnswer,
1184
+ question: HumanInputQuestion | undefined,
1185
+ ): HumanInputAnswerSummary {
1186
+ const optionLabels = new Map(question?.options.map((option) => [option.id, option.label]) ?? []);
1187
+ const values = answer.values.map((value) => optionLabels.get(value) || value);
1188
+ if (answer.other) values.push(answer.other);
1189
+ const fallbackLabel = answer.questionId
1190
+ .replace(/[_-]+/g, " ")
1191
+ .replace(/\b\w/g, (character) => character.toUpperCase());
1192
+ return {
1193
+ questionId: answer.questionId,
1194
+ label: question?.label || question?.prompt || fallbackLabel,
1195
+ prompt: question?.prompt || fallbackLabel,
1196
+ values,
1197
+ };
1198
+ }
1199
+
852
1200
  function providerNativeToolStatus(rawValue: unknown): ToolCallItem["status"] {
853
1201
  const raw = asRecord(rawValue);
854
1202
  if (raw.type !== "hosted_tool_call") {
@@ -972,6 +1320,7 @@ function isActivityItem(item: TimelineItem): item is ActivityItem {
972
1320
  case "tool-call":
973
1321
  case "worker":
974
1322
  case "sandbox":
1323
+ case "startup-phase":
975
1324
  case "memory":
976
1325
  case "fleet-decision":
977
1326
  return true;
@@ -1012,6 +1361,8 @@ type TurnAnchorPrescan = {
1012
1361
  queuedTurnByTrigger: Map<string, string>;
1013
1362
  startSeqByTrigger: Map<string, number>;
1014
1363
  cancelledBeforeStartTriggers: Set<string>;
1364
+ directChatSequenceByTrigger: Map<string, number>;
1365
+ explicitQueuedTriggers: Set<string>;
1015
1366
  startedTurnIds: Set<string>;
1016
1367
  };
1017
1368
 
@@ -1020,18 +1371,61 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
1020
1371
  const queuedTurnByTrigger = new Map<string, string>();
1021
1372
  const startSeqByTrigger = new Map<string, number>();
1022
1373
  const cancelledTurnIds = new Set<string>();
1374
+ const withdrawnTurnIds = new Set<string>();
1023
1375
  const fallbackSeqByTurn = new Map<string, number>();
1376
+ const directChatSequenceByTrigger = new Map<string, number>();
1377
+ const explicitQueuedTriggers = new Set<string>();
1378
+ const queueSteerSequenceByTurn = new Map<string, number>();
1024
1379
  const startedTurnIds = new Set<string>();
1380
+ const userMessageSequenceById = new Map<string, number>();
1025
1381
 
1026
1382
  for (const event of ordered) {
1027
1383
  const payload = asRecord(event.payload);
1028
1384
  const turnId = event.turnId ?? null;
1385
+ if (event.type === "user.message") {
1386
+ userMessageSequenceById.set(event.id, event.sequence);
1387
+ if (payload.routing === "queued_for_execution") {
1388
+ explicitQueuedTriggers.add(event.id);
1389
+ } else if (
1390
+ payload.routing === "accepted_for_execution" ||
1391
+ payload.routing === "accepted_for_steering"
1392
+ ) {
1393
+ directChatSequenceByTrigger.set(event.id, event.sequence);
1394
+ }
1395
+ }
1396
+ if (event.type === "user.message" && payload.delivery === "steer") {
1397
+ directChatSequenceByTrigger.set(event.id, event.sequence);
1398
+ }
1399
+ if (event.type === "session.control.steer_requested") {
1400
+ const targetTurnId = typeof payload.targetTurnId === "string" ? payload.targetTurnId : null;
1401
+ if (targetTurnId && !queueSteerSequenceByTurn.has(targetTurnId)) {
1402
+ queueSteerSequenceByTurn.set(targetTurnId, event.sequence);
1403
+ }
1404
+ }
1405
+ if (
1406
+ event.type === "session.queue.changed" &&
1407
+ (payload.operation === "edit" || payload.operation === "delete")
1408
+ ) {
1409
+ const withdrawnTurnId =
1410
+ typeof payload.turnId === "string" ? payload.turnId : (turnId ?? null);
1411
+ if (withdrawnTurnId) withdrawnTurnIds.add(withdrawnTurnId);
1412
+ }
1029
1413
  if (event.type === "turn.queued") {
1030
1414
  const triggerEventId =
1031
1415
  typeof payload.triggerEventId === "string" ? payload.triggerEventId : null;
1032
1416
  const queuedTurnId = typeof payload.turnId === "string" ? payload.turnId : turnId;
1033
1417
  if (triggerEventId && queuedTurnId) {
1034
1418
  queuedTurnByTrigger.set(triggerEventId, queuedTurnId);
1419
+ if (
1420
+ (payload.routing === "accepted_for_execution" ||
1421
+ payload.routing === "accepted_for_steering") &&
1422
+ !directChatSequenceByTrigger.has(triggerEventId)
1423
+ ) {
1424
+ directChatSequenceByTrigger.set(
1425
+ triggerEventId,
1426
+ userMessageSequenceById.get(triggerEventId) ?? event.sequence,
1427
+ );
1428
+ }
1035
1429
  }
1036
1430
  continue;
1037
1431
  }
@@ -1060,6 +1454,10 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
1060
1454
  }
1061
1455
 
1062
1456
  for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
1457
+ const queueSteerSequence = queueSteerSequenceByTurn.get(turnId);
1458
+ if (queueSteerSequence !== undefined) {
1459
+ directChatSequenceByTrigger.set(triggerEventId, queueSteerSequence);
1460
+ }
1063
1461
  if (!startSeqByTrigger.has(triggerEventId)) {
1064
1462
  const fallbackSeq = fallbackSeqByTurn.get(turnId);
1065
1463
  if (fallbackSeq !== undefined) {
@@ -1071,7 +1469,7 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
1071
1469
  const cancelledBeforeStartTriggers = new Set<string>();
1072
1470
  for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
1073
1471
  if (
1074
- cancelledTurnIds.has(turnId) &&
1472
+ (cancelledTurnIds.has(turnId) || withdrawnTurnIds.has(turnId)) &&
1075
1473
  !startSeqByTrigger.has(triggerEventId) &&
1076
1474
  !fallbackSeqByTurn.has(turnId)
1077
1475
  ) {
@@ -1083,6 +1481,8 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
1083
1481
  queuedTurnByTrigger,
1084
1482
  startSeqByTrigger,
1085
1483
  cancelledBeforeStartTriggers,
1484
+ directChatSequenceByTrigger,
1485
+ explicitQueuedTriggers,
1086
1486
  startedTurnIds,
1087
1487
  };
1088
1488
  }
@@ -1095,8 +1495,22 @@ function orderTimelineEvents(events: SessionEvent[], prescan: TurnAnchorPrescan)
1095
1495
  if (event.type !== "user.message") {
1096
1496
  continue;
1097
1497
  }
1498
+ // Steer is an accepted direction change, not waiting queue copy. Keep it
1499
+ // directly in chat across refresh/reconnect even before its turn starts.
1500
+ const directChatSequence = prescan.directChatSequenceByTrigger.get(event.id);
1501
+ if (directChatSequence !== undefined) {
1502
+ pushInsertion(insertions, directChatSequence, event);
1503
+ continue;
1504
+ }
1098
1505
  const queuedTurnId = prescan.queuedTurnByTrigger.get(event.id);
1099
1506
  if (!queuedTurnId) {
1507
+ if (prescan.explicitQueuedTriggers.has(event.id)) {
1508
+ const startSeq = prescan.startSeqByTrigger.get(event.id);
1509
+ if (startSeq !== undefined) {
1510
+ pushInsertion(insertions, startSeq, event);
1511
+ }
1512
+ continue;
1513
+ }
1100
1514
  pushInsertion(insertions, event.sequence, event);
1101
1515
  continue;
1102
1516
  }
@@ -1160,6 +1574,7 @@ function isTurnExecutionEvidence(type: string): boolean {
1160
1574
  type === "session.requiresAction" ||
1161
1575
  type === "tool.auth_needed" ||
1162
1576
  type === "credential.auth_needed" ||
1577
+ type.startsWith("turn.startup.phase.") ||
1163
1578
  type.startsWith("rig.setup.") ||
1164
1579
  type === "codex.capacity.waiting" ||
1165
1580
  type === "codex.capacity.resumed" ||
@@ -1265,7 +1680,12 @@ function foldSettledTurn(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
1265
1680
  }
1266
1681
 
1267
1682
  const finalMessage = extractFinalAgentMessage(collected, turnEnd);
1268
- const body = finalMessage ? collected.slice(0, -1) : collected;
1683
+ const fallbackMessage =
1684
+ finalMessage || hasOrdinaryFinalAgentMessage(collected, turnEnd)
1685
+ ? null
1686
+ : extractLatestCompletedCommentary(collected, turnEnd);
1687
+ const visibleMessage = finalMessage ?? fallbackMessage;
1688
+ const body = visibleMessage ? collected.filter((group) => group !== visibleMessage) : collected;
1269
1689
  if (body.length === 0) {
1270
1690
  return;
1271
1691
  }
@@ -1294,7 +1714,7 @@ function foldSettledTurn(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
1294
1714
  groups.splice(
1295
1715
  startIndex,
1296
1716
  collected.length,
1297
- ...(finalMessage ? [turnGroup, finalMessage] : [turnGroup]),
1717
+ ...(visibleMessage ? [turnGroup, visibleMessage] : [turnGroup]),
1298
1718
  );
1299
1719
  }
1300
1720
 
@@ -1303,6 +1723,7 @@ function isTurnBoundary(group: TimelineGroup | undefined): boolean {
1303
1723
  group?.kind === "turn" ||
1304
1724
  (group?.kind === "item" &&
1305
1725
  (group.item.kind === "user-message" ||
1726
+ group.item.kind === "human-input" ||
1306
1727
  group.item.kind === "context-compaction" ||
1307
1728
  (group.item.kind === "machine-input-batch" &&
1308
1729
  group.item.members.some((member) => member.kind === "media_generation_result")) ||
@@ -1350,6 +1771,43 @@ function extractFinalAgentMessage(
1350
1771
  return tail;
1351
1772
  }
1352
1773
 
1774
+ function hasOrdinaryFinalAgentMessage(groups: TimelineGroup[], turnEnd: TurnEndItem): boolean {
1775
+ return groups.some((group) => {
1776
+ if (group.kind !== "item" || group.item.kind !== "agent-message") return false;
1777
+ const message = group.item;
1778
+ return (
1779
+ !message.streaming &&
1780
+ message.phase !== "commentary" &&
1781
+ message.text.trim().length > 0 &&
1782
+ belongsToTurn(message, turnEnd.turnId)
1783
+ );
1784
+ });
1785
+ }
1786
+
1787
+ function extractLatestCompletedCommentary(
1788
+ groups: TimelineGroup[],
1789
+ turnEnd: TurnEndItem,
1790
+ ): Extract<TimelineGroup, { kind: "item" }> | null {
1791
+ for (let index = groups.length - 1; index >= 0; index -= 1) {
1792
+ const group = groups[index];
1793
+ if (group?.kind !== "item" || group.item.kind !== "agent-message") continue;
1794
+ const message = group.item;
1795
+ if (
1796
+ !message.streaming &&
1797
+ message.phase === "commentary" &&
1798
+ message.text.trim().length > 0 &&
1799
+ belongsToTurn(message, turnEnd.turnId)
1800
+ ) {
1801
+ return group;
1802
+ }
1803
+ }
1804
+ return null;
1805
+ }
1806
+
1807
+ function belongsToTurn(message: AgentMessageItem, turnId: string | null): boolean {
1808
+ return !message.turnId || !turnId || message.turnId === turnId;
1809
+ }
1810
+
1353
1811
  function groupStartedAt(group: TimelineGroup | undefined): string | undefined {
1354
1812
  if (!group) {
1355
1813
  return undefined;
@@ -1372,6 +1830,10 @@ function stringValue(value: unknown): string {
1372
1830
  return typeof value === "string" ? value : "";
1373
1831
  }
1374
1832
 
1833
+ function assistantMessagePhase(value: unknown): AgentMessageItem["phase"] {
1834
+ return value === "commentary" || value === "final_answer" ? value : undefined;
1835
+ }
1836
+
1375
1837
  function numberOrNull(value: unknown): number | null {
1376
1838
  return typeof value === "number" && Number.isFinite(value) ? value : null;
1377
1839
  }
@@ -1435,6 +1897,11 @@ function machineInputMembers(value: unknown): MachineInputBatchItem["members"] {
1435
1897
  "agent_steer_instruction",
1436
1898
  "child_terminal_result",
1437
1899
  "media_generation_result",
1900
+ "child_requires_action",
1901
+ "child_requires_action_resolved",
1902
+ "child_paused",
1903
+ "child_waiting_capacity",
1904
+ "child_progress",
1438
1905
  ]);
1439
1906
  const classifications = new Set<MachineInputBatchItem["members"][number]["classification"]>([
1440
1907
  "success",
@@ -1639,6 +2106,38 @@ function findOpenSandbox(items: TimelineItem[], name: string): SandboxItem | und
1639
2106
  );
1640
2107
  }
1641
2108
 
2109
+ function startupPhaseForSandboxOperation(name: string): StartupPhase | null {
2110
+ return SANDBOX_STARTUP_PHASES[name] ?? null;
2111
+ }
2112
+
2113
+ function startupPhaseFromPayload(value: unknown): StartupPhase | null {
2114
+ return typeof value === "string" && STARTUP_PHASES.has(value as StartupPhase)
2115
+ ? (value as StartupPhase)
2116
+ : null;
2117
+ }
2118
+
2119
+ const SANDBOX_STARTUP_PHASES: Record<string, StartupPhase> = {
2120
+ "sandbox.provision": "sandbox",
2121
+ "repository-clone": "repository",
2122
+ "file-resource-download": "files",
2123
+ };
2124
+ const STARTUP_PHASES = new Set<StartupPhase>([
2125
+ "queue",
2126
+ "sandbox",
2127
+ "rig",
2128
+ "repository",
2129
+ "files",
2130
+ "tools",
2131
+ "model_preparation",
2132
+ "provider_first_byte",
2133
+ ]);
2134
+
2135
+ function elapsedDurationMs(startedAt: string, completedAt: string): number | null {
2136
+ const start = Date.parse(startedAt);
2137
+ const end = Date.parse(completedAt);
2138
+ return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : null;
2139
+ }
2140
+
1642
2141
  function failureMessage(payload: Record<string, unknown>): string | null {
1643
2142
  for (const key of ["error", "message"] as const) {
1644
2143
  const value = payload[key];
@@ -1675,7 +2174,12 @@ function shouldSuppressAgentGoalLandmark(type: string, payload: Record<string, u
1675
2174
  if (type === "goal.completed") {
1676
2175
  return true;
1677
2176
  }
1678
- if (type === "goal.set" || type === "goal.updated" || type === "goal.paused") {
2177
+ if (
2178
+ type === "goal.set" ||
2179
+ type === "goal.updated" ||
2180
+ type === "goal.paused" ||
2181
+ type === "goal.held"
2182
+ ) {
1679
2183
  return payload.actor === "agent";
1680
2184
  }
1681
2185
  return false;