@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.
- package/README.md +33 -10
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
- package/dist/chunk-LS4POUTY.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
- package/dist/chunk-RSXJN73N.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
- package/dist/chunk-SE4W4WQ3.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +6 -3
- package/dist/realtime.js +36 -14
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +50 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +103 -59
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +215 -18
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +53 -17
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +529 -25
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +77 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /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
|
-
?
|
|
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:
|
|
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
|
|
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
|
-
...(
|
|
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 (
|
|
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;
|