@opengeni/react 1.0.1 → 2.2.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-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
  8. package/dist/chunk-5RZN3DD5.js.map +1 -0
  9. package/dist/chunk-5YK2ZDIG.js +116 -0
  10. package/dist/chunk-5YK2ZDIG.js.map +1 -0
  11. package/dist/chunk-H5YGYNXS.js +329 -0
  12. package/dist/chunk-H5YGYNXS.js.map +1 -0
  13. package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
  14. package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
  15. package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
  16. package/dist/chunk-JOAVA2TW.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-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
  24. package/dist/chunk-UOLJWT5Y.js.map +1 -0
  25. package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
  26. package/dist/chunk-W6NFJXSA.js.map +1 -0
  27. package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
  28. package/dist/chunk-X4A66EWZ.js.map +1 -0
  29. package/dist/chunk-X4JAFD5R.js +3907 -0
  30. package/dist/chunk-X4JAFD5R.js.map +1 -0
  31. package/dist/{chunk-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
  32. package/dist/chunk-XSA2QAKP.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 +48 -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 +91 -55
  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 +162 -17
  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 +476 -23
  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 +75 -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;
@@ -349,6 +493,12 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
349
493
  const callId = typeof payload.id === "string" ? payload.id : null;
350
494
  const args = payload.arguments ?? null;
351
495
  closeStreamingTail();
496
+ if (callId && humanInputToolCallIds.has(callId)) {
497
+ // The matching session.humanInput.requested / response lifecycle owns
498
+ // the visible chat card. Rendering the raw tool too would hide the
499
+ // decision behind a duplicate generic "Ask" step.
500
+ break;
501
+ }
352
502
  if (
353
503
  toolMatchesLeaf(name, WORKER_SPAWN_TOOL) ||
354
504
  toolMatchesLeaf(name, WORKER_MESSAGE_TOOL)
@@ -452,15 +602,58 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
452
602
  break;
453
603
  }
454
604
 
605
+ case "turn.queued": {
606
+ const queuedTurnId = turnId ?? (typeof payload.turnId === "string" ? payload.turnId : null);
607
+ if (queuedTurnId) {
608
+ queuedAtByTurn.set(queuedTurnId, event.occurredAt);
609
+ }
610
+ break;
611
+ }
612
+
613
+ case "turn.started": {
614
+ if (!turnId) break;
615
+ const queuedAt = queuedAtByTurn.get(turnId);
616
+ if (!queuedAt) break;
617
+ // Recovery starts the same logical turn again. Queue latency belongs to
618
+ // the first admission only, so consume the queued timestamp after its
619
+ // first projection instead of manufacturing another queue span.
620
+ queuedAtByTurn.delete(turnId);
621
+ closeStreamingTail();
622
+ settleStartupPhase(
623
+ "queue",
624
+ "complete",
625
+ elapsedDurationMs(queuedAt, event.occurredAt),
626
+ null,
627
+ 0,
628
+ queuedAt,
629
+ `${event.id}-queue`,
630
+ );
631
+ break;
632
+ }
633
+
455
634
  case "sandbox.operation.started":
456
635
  case "sandbox.operation.completed":
457
636
  case "sandbox.operation.failed": {
458
637
  const name = typeof payload.name === "string" ? payload.name : "sandbox";
459
638
  const status = event.type.endsWith(".failed")
460
- ? "failed"
639
+ ? payload.expectedTransition === true
640
+ ? "cancelled"
641
+ : "failed"
461
642
  : event.type.endsWith(".completed")
462
643
  ? "complete"
463
644
  : "running";
645
+ const origin =
646
+ payload.origin === "created" ||
647
+ payload.origin === "restored" ||
648
+ payload.origin === "resumed"
649
+ ? payload.origin
650
+ : null;
651
+ const startupPhase = startupPhaseForSandboxOperation(name);
652
+ if (startupPhase) {
653
+ closeStreamingTail();
654
+ settleStartupPhase(startupPhase, status, numberOrNull(payload.durationMs), origin);
655
+ break;
656
+ }
464
657
  // Routine per-turn platform plumbing that runs before EVERY turn to
465
658
  // guarantee box contents survive a re-warm — NOT the agent redoing work:
466
659
  // - repository-clone: idempotent clone check + off-manifest token re-seed;
@@ -480,13 +673,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
480
673
  const existing = findOpenSandbox(items, name);
481
674
  if (existing && status !== "running") {
482
675
  existing.status = status;
483
- if (
484
- payload.origin === "created" ||
485
- payload.origin === "restored" ||
486
- payload.origin === "resumed"
487
- ) {
488
- existing.origin = payload.origin;
489
- }
676
+ if (origin) existing.origin = origin;
490
677
  const message = failureMessage(payload);
491
678
  if (message) {
492
679
  existing.output = existing.output ? `${existing.output}\n${message}` : message;
@@ -502,12 +689,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
502
689
  name,
503
690
  command: typeof payload.command === "string" ? payload.command : null,
504
691
  output: failureMessage(payload) ?? "",
505
- origin:
506
- payload.origin === "created" ||
507
- payload.origin === "restored" ||
508
- payload.origin === "resumed"
509
- ? payload.origin
510
- : null,
692
+ origin,
511
693
  status,
512
694
  occurredAt: event.occurredAt,
513
695
  });
@@ -515,6 +697,29 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
515
697
  break;
516
698
  }
517
699
 
700
+ case "agent.model.request": {
701
+ const requestPhase = typeof payload.phase === "string" ? payload.phase : null;
702
+ const status =
703
+ requestPhase === "started"
704
+ ? "running"
705
+ : requestPhase === "first_byte" || requestPhase === "first_event"
706
+ ? "complete"
707
+ : requestPhase === "failed" || requestPhase === "timed_out"
708
+ ? "failed"
709
+ : null;
710
+ if (status) {
711
+ if (status === "running") closeStreamingTail();
712
+ settleStartupPhase(
713
+ "provider_first_byte",
714
+ status,
715
+ status === "running" ? null : numberOrNull(payload.durationMs),
716
+ null,
717
+ status === "running" ? 2 : 1,
718
+ );
719
+ }
720
+ break;
721
+ }
722
+
518
723
  case "sandbox.command.output.delta": {
519
724
  // `chunk` is the canonical wire field; text/output are legacy shapes.
520
725
  const text =
@@ -572,7 +777,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
572
777
  }
573
778
 
574
779
  case "session.requiresAction": {
575
- finalizeOpen(turnId);
780
+ finalizeOpen(turnId, "complete", event.occurredAt);
576
781
  items.push({
577
782
  kind: "notice",
578
783
  id: event.id,
@@ -639,6 +844,12 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
639
844
  // Recovery requests are durable control-plane evidence, not a user
640
845
  // message or proof that recovery succeeded. Live state already exposes
641
846
  // a genuinely recovering session; keep the raw event in Debug/audit.
847
+ if (turnId) {
848
+ startupRecoveryRevisionByTurn.set(
849
+ turnId,
850
+ (startupRecoveryRevisionByTurn.get(turnId) ?? 0) + 1,
851
+ );
852
+ }
642
853
  break;
643
854
  }
644
855
 
@@ -700,7 +911,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
700
911
  // adding a generic turn chip would falsely make it look like an
701
912
  // extra agent response.
702
913
  if (payload.maintenance === "context_compaction") {
703
- finalizeOpen(turnId);
914
+ finalizeOpen(turnId, "complete", event.occurredAt);
704
915
  break;
705
916
  }
706
917
  // Credit exhaustion arrives as a NOMINALLY completed turn (`detail:
@@ -710,7 +921,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
710
921
  // session looking healthy while every future turn silently dies, so it
711
922
  // projects exactly like a failed turn plus an explicit notice.
712
923
  if (isCreditExhaustionPayload(payload)) {
713
- finalizeOpen(turnId);
924
+ finalizeOpen(turnId, "complete", event.occurredAt);
714
925
  items.push(turnEndItem(event, "failed", CREDIT_EXHAUSTION_MESSAGE));
715
926
  items.push({
716
927
  kind: "notice",
@@ -721,7 +932,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
721
932
  });
722
933
  break;
723
934
  }
724
- finalizeOpen(turnId);
935
+ finalizeOpen(turnId, "complete", event.occurredAt);
725
936
  items.push(turnEndItem(event, "complete", null));
726
937
  break;
727
938
  }
@@ -738,7 +949,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
738
949
  // spent once, on the turn-level outcome. Items caught mid-flight read
739
950
  // as calm "interrupted" (same as turn.cancelled); an item that itself
740
951
  // failed keeps its own failed status from its output event.
741
- finalizeOpen(turnId, "cancelled");
952
+ finalizeOpen(turnId, "cancelled", event.occurredAt);
742
953
  items.push(turnEndItem(event, "failed", failureText));
743
954
  if (!hadActivity) {
744
955
  items.push({
@@ -760,7 +971,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
760
971
  break;
761
972
  }
762
973
  const hadActivity = hasTurnActivity(items, turnId);
763
- finalizeOpen(turnId, "cancelled");
974
+ finalizeOpen(turnId, "cancelled", event.occurredAt);
764
975
  items.push(turnEndItem(event, "cancelled", null));
765
976
  if (!hadActivity) {
766
977
  items.push({
@@ -802,6 +1013,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
802
1013
  case "goal.paused":
803
1014
  case "goal.resumed":
804
1015
  case "goal.cleared":
1016
+ case "goal.held":
805
1017
  case "goal.continuation": {
806
1018
  // Agent tool mutations already appear as tool-call rows in the activity
807
1019
  // cluster. Re-emitting them as GoalRow landmarks splits "N steps" mid-turn.
@@ -812,7 +1024,10 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
812
1024
  kind: "goal",
813
1025
  id: event.id,
814
1026
  action: event.type.slice("goal.".length) as GoalItem["action"],
815
- text: goalText(payload),
1027
+ text:
1028
+ event.type === "goal.held" && typeof payload.reason === "string" && payload.reason
1029
+ ? payload.reason
1030
+ : goalText(payload),
816
1031
  occurredAt: event.occurredAt,
817
1032
  });
818
1033
  break;
@@ -849,6 +1064,134 @@ function realtimeVoiceMessage(payload: Record<string, unknown>): {
849
1064
  return null;
850
1065
  }
851
1066
 
1067
+ function humanInputRequestsById(events: SessionEvent[]): Map<string, Record<string, unknown>> {
1068
+ const requests = new Map<string, Record<string, unknown>>();
1069
+ for (const event of events) {
1070
+ if (event.type !== "session.humanInput.requested") continue;
1071
+ const request = asRecord(asRecord(event.payload).request);
1072
+ const requestId = stringValue(request.id);
1073
+ if (requestId) requests.set(requestId, request);
1074
+ }
1075
+ return requests;
1076
+ }
1077
+
1078
+ function humanInputItemFromResponse(
1079
+ event: SessionEvent,
1080
+ payload: Record<string, unknown>,
1081
+ requests: Map<string, Record<string, unknown>>,
1082
+ ): HumanInputItem | null {
1083
+ const response = asRecord(payload.response);
1084
+ const outcome = response.outcome;
1085
+ if (
1086
+ outcome !== "answered" &&
1087
+ outcome !== "skipped" &&
1088
+ outcome !== "expired" &&
1089
+ outcome !== "cancelled"
1090
+ ) {
1091
+ return null;
1092
+ }
1093
+
1094
+ const requestId = stringValue(payload.requestId);
1095
+ const request = requests.get(requestId);
1096
+ const questions = humanInputQuestions(request?.questions);
1097
+ const normalizedResponse: HumanInputResponse =
1098
+ outcome === "answered"
1099
+ ? {
1100
+ outcome,
1101
+ answers: Array.isArray(response.answers)
1102
+ ? response.answers
1103
+ .map(asRecord)
1104
+ .map(humanInputAnswer)
1105
+ .filter((answer): answer is HumanInputAnswer => answer !== null)
1106
+ : [],
1107
+ }
1108
+ : { outcome };
1109
+ const questionsById = new Map(questions.map((question) => [question.id, question]));
1110
+ const answers =
1111
+ normalizedResponse.outcome === "answered"
1112
+ ? normalizedResponse.answers.map((answer) =>
1113
+ humanInputAnswerSummary(answer, questionsById.get(answer.questionId)),
1114
+ )
1115
+ : [];
1116
+ return {
1117
+ kind: "human-input",
1118
+ id: event.id,
1119
+ turnId: event.turnId ?? null,
1120
+ requestId,
1121
+ questions,
1122
+ response: normalizedResponse,
1123
+ answers,
1124
+ occurredAt: event.occurredAt,
1125
+ };
1126
+ }
1127
+
1128
+ function humanInputQuestions(value: unknown): HumanInputQuestion[] {
1129
+ if (!Array.isArray(value)) return [];
1130
+ return value.map(asRecord).flatMap((question) => {
1131
+ const id = stringValue(question.id);
1132
+ const prompt = stringValue(question.prompt);
1133
+ const kind = question.kind;
1134
+ if (
1135
+ !id ||
1136
+ !prompt ||
1137
+ (kind !== "text" && kind !== "single_select" && kind !== "multi_select")
1138
+ ) {
1139
+ return [];
1140
+ }
1141
+ const options = Array.isArray(question.options)
1142
+ ? question.options.map(asRecord).flatMap((option) => {
1143
+ const optionId = stringValue(option.id);
1144
+ const label = stringValue(option.label);
1145
+ return optionId && label
1146
+ ? [{ id: optionId, label, description: stringValue(option.description) || null }]
1147
+ : [];
1148
+ })
1149
+ : [];
1150
+ return [
1151
+ {
1152
+ id,
1153
+ kind,
1154
+ prompt,
1155
+ label: stringValue(question.label) || null,
1156
+ helpText: stringValue(question.helpText) || null,
1157
+ options,
1158
+ required: question.required === true,
1159
+ allowOther: question.allowOther === true,
1160
+ },
1161
+ ];
1162
+ });
1163
+ }
1164
+
1165
+ function humanInputAnswer(value: Record<string, unknown>): HumanInputAnswer | null {
1166
+ const questionId = stringValue(value.questionId);
1167
+ if (!questionId) return null;
1168
+ return {
1169
+ questionId,
1170
+ values: Array.isArray(value.values)
1171
+ ? value.values.filter((answer): answer is string => typeof answer === "string")
1172
+ : [],
1173
+ ...(typeof value.other === "string" && value.other ? { other: value.other } : {}),
1174
+ };
1175
+ }
1176
+
1177
+ function humanInputAnswerSummary(
1178
+ answer: HumanInputAnswer,
1179
+ question: HumanInputQuestion | undefined,
1180
+ ): HumanInputAnswerSummary {
1181
+ const optionLabels = new Map(question?.options.map((option) => [option.id, option.label]) ?? []);
1182
+ const values = answer.values.map((value) => optionLabels.get(value) || value);
1183
+ if (answer.other) values.push(answer.other);
1184
+ const fallbackLabel = answer.questionId
1185
+ .replace(/[_-]+/g, " ")
1186
+ .replace(/\b\w/g, (character) => character.toUpperCase());
1187
+ return {
1188
+ questionId: answer.questionId,
1189
+ label: question?.label || question?.prompt || fallbackLabel,
1190
+ prompt: question?.prompt || fallbackLabel,
1191
+ values,
1192
+ };
1193
+ }
1194
+
852
1195
  function providerNativeToolStatus(rawValue: unknown): ToolCallItem["status"] {
853
1196
  const raw = asRecord(rawValue);
854
1197
  if (raw.type !== "hosted_tool_call") {
@@ -972,6 +1315,7 @@ function isActivityItem(item: TimelineItem): item is ActivityItem {
972
1315
  case "tool-call":
973
1316
  case "worker":
974
1317
  case "sandbox":
1318
+ case "startup-phase":
975
1319
  case "memory":
976
1320
  case "fleet-decision":
977
1321
  return true;
@@ -1012,6 +1356,8 @@ type TurnAnchorPrescan = {
1012
1356
  queuedTurnByTrigger: Map<string, string>;
1013
1357
  startSeqByTrigger: Map<string, number>;
1014
1358
  cancelledBeforeStartTriggers: Set<string>;
1359
+ directChatSequenceByTrigger: Map<string, number>;
1360
+ explicitQueuedTriggers: Set<string>;
1015
1361
  startedTurnIds: Set<string>;
1016
1362
  };
1017
1363
 
@@ -1020,18 +1366,61 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
1020
1366
  const queuedTurnByTrigger = new Map<string, string>();
1021
1367
  const startSeqByTrigger = new Map<string, number>();
1022
1368
  const cancelledTurnIds = new Set<string>();
1369
+ const withdrawnTurnIds = new Set<string>();
1023
1370
  const fallbackSeqByTurn = new Map<string, number>();
1371
+ const directChatSequenceByTrigger = new Map<string, number>();
1372
+ const explicitQueuedTriggers = new Set<string>();
1373
+ const queueSteerSequenceByTurn = new Map<string, number>();
1024
1374
  const startedTurnIds = new Set<string>();
1375
+ const userMessageSequenceById = new Map<string, number>();
1025
1376
 
1026
1377
  for (const event of ordered) {
1027
1378
  const payload = asRecord(event.payload);
1028
1379
  const turnId = event.turnId ?? null;
1380
+ if (event.type === "user.message") {
1381
+ userMessageSequenceById.set(event.id, event.sequence);
1382
+ if (payload.routing === "queued_for_execution") {
1383
+ explicitQueuedTriggers.add(event.id);
1384
+ } else if (
1385
+ payload.routing === "accepted_for_execution" ||
1386
+ payload.routing === "accepted_for_steering"
1387
+ ) {
1388
+ directChatSequenceByTrigger.set(event.id, event.sequence);
1389
+ }
1390
+ }
1391
+ if (event.type === "user.message" && payload.delivery === "steer") {
1392
+ directChatSequenceByTrigger.set(event.id, event.sequence);
1393
+ }
1394
+ if (event.type === "session.control.steer_requested") {
1395
+ const targetTurnId = typeof payload.targetTurnId === "string" ? payload.targetTurnId : null;
1396
+ if (targetTurnId && !queueSteerSequenceByTurn.has(targetTurnId)) {
1397
+ queueSteerSequenceByTurn.set(targetTurnId, event.sequence);
1398
+ }
1399
+ }
1400
+ if (
1401
+ event.type === "session.queue.changed" &&
1402
+ (payload.operation === "edit" || payload.operation === "delete")
1403
+ ) {
1404
+ const withdrawnTurnId =
1405
+ typeof payload.turnId === "string" ? payload.turnId : (turnId ?? null);
1406
+ if (withdrawnTurnId) withdrawnTurnIds.add(withdrawnTurnId);
1407
+ }
1029
1408
  if (event.type === "turn.queued") {
1030
1409
  const triggerEventId =
1031
1410
  typeof payload.triggerEventId === "string" ? payload.triggerEventId : null;
1032
1411
  const queuedTurnId = typeof payload.turnId === "string" ? payload.turnId : turnId;
1033
1412
  if (triggerEventId && queuedTurnId) {
1034
1413
  queuedTurnByTrigger.set(triggerEventId, queuedTurnId);
1414
+ if (
1415
+ (payload.routing === "accepted_for_execution" ||
1416
+ payload.routing === "accepted_for_steering") &&
1417
+ !directChatSequenceByTrigger.has(triggerEventId)
1418
+ ) {
1419
+ directChatSequenceByTrigger.set(
1420
+ triggerEventId,
1421
+ userMessageSequenceById.get(triggerEventId) ?? event.sequence,
1422
+ );
1423
+ }
1035
1424
  }
1036
1425
  continue;
1037
1426
  }
@@ -1060,6 +1449,10 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
1060
1449
  }
1061
1450
 
1062
1451
  for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
1452
+ const queueSteerSequence = queueSteerSequenceByTurn.get(turnId);
1453
+ if (queueSteerSequence !== undefined) {
1454
+ directChatSequenceByTrigger.set(triggerEventId, queueSteerSequence);
1455
+ }
1063
1456
  if (!startSeqByTrigger.has(triggerEventId)) {
1064
1457
  const fallbackSeq = fallbackSeqByTurn.get(turnId);
1065
1458
  if (fallbackSeq !== undefined) {
@@ -1071,7 +1464,7 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
1071
1464
  const cancelledBeforeStartTriggers = new Set<string>();
1072
1465
  for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
1073
1466
  if (
1074
- cancelledTurnIds.has(turnId) &&
1467
+ (cancelledTurnIds.has(turnId) || withdrawnTurnIds.has(turnId)) &&
1075
1468
  !startSeqByTrigger.has(triggerEventId) &&
1076
1469
  !fallbackSeqByTurn.has(turnId)
1077
1470
  ) {
@@ -1083,6 +1476,8 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
1083
1476
  queuedTurnByTrigger,
1084
1477
  startSeqByTrigger,
1085
1478
  cancelledBeforeStartTriggers,
1479
+ directChatSequenceByTrigger,
1480
+ explicitQueuedTriggers,
1086
1481
  startedTurnIds,
1087
1482
  };
1088
1483
  }
@@ -1095,8 +1490,22 @@ function orderTimelineEvents(events: SessionEvent[], prescan: TurnAnchorPrescan)
1095
1490
  if (event.type !== "user.message") {
1096
1491
  continue;
1097
1492
  }
1493
+ // Steer is an accepted direction change, not waiting queue copy. Keep it
1494
+ // directly in chat across refresh/reconnect even before its turn starts.
1495
+ const directChatSequence = prescan.directChatSequenceByTrigger.get(event.id);
1496
+ if (directChatSequence !== undefined) {
1497
+ pushInsertion(insertions, directChatSequence, event);
1498
+ continue;
1499
+ }
1098
1500
  const queuedTurnId = prescan.queuedTurnByTrigger.get(event.id);
1099
1501
  if (!queuedTurnId) {
1502
+ if (prescan.explicitQueuedTriggers.has(event.id)) {
1503
+ const startSeq = prescan.startSeqByTrigger.get(event.id);
1504
+ if (startSeq !== undefined) {
1505
+ pushInsertion(insertions, startSeq, event);
1506
+ }
1507
+ continue;
1508
+ }
1100
1509
  pushInsertion(insertions, event.sequence, event);
1101
1510
  continue;
1102
1511
  }
@@ -1160,6 +1569,7 @@ function isTurnExecutionEvidence(type: string): boolean {
1160
1569
  type === "session.requiresAction" ||
1161
1570
  type === "tool.auth_needed" ||
1162
1571
  type === "credential.auth_needed" ||
1572
+ type.startsWith("turn.startup.phase.") ||
1163
1573
  type.startsWith("rig.setup.") ||
1164
1574
  type === "codex.capacity.waiting" ||
1165
1575
  type === "codex.capacity.resumed" ||
@@ -1303,6 +1713,7 @@ function isTurnBoundary(group: TimelineGroup | undefined): boolean {
1303
1713
  group?.kind === "turn" ||
1304
1714
  (group?.kind === "item" &&
1305
1715
  (group.item.kind === "user-message" ||
1716
+ group.item.kind === "human-input" ||
1306
1717
  group.item.kind === "context-compaction" ||
1307
1718
  (group.item.kind === "machine-input-batch" &&
1308
1719
  group.item.members.some((member) => member.kind === "media_generation_result")) ||
@@ -1435,6 +1846,11 @@ function machineInputMembers(value: unknown): MachineInputBatchItem["members"] {
1435
1846
  "agent_steer_instruction",
1436
1847
  "child_terminal_result",
1437
1848
  "media_generation_result",
1849
+ "child_requires_action",
1850
+ "child_requires_action_resolved",
1851
+ "child_paused",
1852
+ "child_waiting_capacity",
1853
+ "child_progress",
1438
1854
  ]);
1439
1855
  const classifications = new Set<MachineInputBatchItem["members"][number]["classification"]>([
1440
1856
  "success",
@@ -1639,6 +2055,38 @@ function findOpenSandbox(items: TimelineItem[], name: string): SandboxItem | und
1639
2055
  );
1640
2056
  }
1641
2057
 
2058
+ function startupPhaseForSandboxOperation(name: string): StartupPhase | null {
2059
+ return SANDBOX_STARTUP_PHASES[name] ?? null;
2060
+ }
2061
+
2062
+ function startupPhaseFromPayload(value: unknown): StartupPhase | null {
2063
+ return typeof value === "string" && STARTUP_PHASES.has(value as StartupPhase)
2064
+ ? (value as StartupPhase)
2065
+ : null;
2066
+ }
2067
+
2068
+ const SANDBOX_STARTUP_PHASES: Record<string, StartupPhase> = {
2069
+ "sandbox.provision": "sandbox",
2070
+ "repository-clone": "repository",
2071
+ "file-resource-download": "files",
2072
+ };
2073
+ const STARTUP_PHASES = new Set<StartupPhase>([
2074
+ "queue",
2075
+ "sandbox",
2076
+ "rig",
2077
+ "repository",
2078
+ "files",
2079
+ "tools",
2080
+ "model_preparation",
2081
+ "provider_first_byte",
2082
+ ]);
2083
+
2084
+ function elapsedDurationMs(startedAt: string, completedAt: string): number | null {
2085
+ const start = Date.parse(startedAt);
2086
+ const end = Date.parse(completedAt);
2087
+ return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : null;
2088
+ }
2089
+
1642
2090
  function failureMessage(payload: Record<string, unknown>): string | null {
1643
2091
  for (const key of ["error", "message"] as const) {
1644
2092
  const value = payload[key];
@@ -1675,7 +2123,12 @@ function shouldSuppressAgentGoalLandmark(type: string, payload: Record<string, u
1675
2123
  if (type === "goal.completed") {
1676
2124
  return true;
1677
2125
  }
1678
- if (type === "goal.set" || type === "goal.updated" || type === "goal.paused") {
2126
+ if (
2127
+ type === "goal.set" ||
2128
+ type === "goal.updated" ||
2129
+ type === "goal.paused" ||
2130
+ type === "goal.held"
2131
+ ) {
1679
2132
  return payload.actor === "agent";
1680
2133
  }
1681
2134
  return false;