@opengeni/react 0.12.0 → 0.15.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 +20 -14
- package/dist/chunk-TOJR776I.js +2280 -0
- package/dist/chunk-TOJR776I.js.map +1 -0
- package/dist/index.d.ts +1040 -363
- package/dist/index.js +10938 -6281
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-BpdwuQcD.d.ts} +134 -14
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +23 -1
- package/package.json +20 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +41 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +417 -69
- package/src/components/code-editor.tsx +28 -36
- package/src/components/command-palette.tsx +26 -5
- package/src/components/desktop-viewer.tsx +29 -16
- package/src/components/diff-view.tsx +27 -189
- package/src/components/enrollment-consent.tsx +28 -10
- package/src/components/enrollment-device-flow.tsx +8 -5
- package/src/components/file-browser.tsx +190 -56
- package/src/components/fleet-tile.tsx +13 -4
- package/src/components/machine-card.tsx +93 -15
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-health-pill.tsx +68 -0
- package/src/components/machine-metrics.tsx +28 -19
- package/src/components/machine-status-pill.tsx +26 -5
- package/src/components/machines/health.ts +146 -0
- package/src/components/machines/machine-detail.tsx +220 -0
- package/src/components/machines/metric-history-chart.tsx +298 -0
- package/src/components/machines/metric-sparkline.tsx +76 -0
- package/src/components/machines/series.ts +113 -0
- package/src/components/machines-dashboard.tsx +21 -3
- package/src/components/markdown.tsx +39 -9
- package/src/components/message-timeline.tsx +633 -109
- package/src/components/pierre-diff.tsx +83 -15
- package/src/components/pierre-file.tsx +10 -9
- package/src/components/queue-surface.tsx +578 -0
- package/src/components/sandbox-files.tsx +193 -225
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +957 -0
- package/src/components/session-status.tsx +22 -2
- package/src/components/workbench-changes.tsx +585 -0
- package/src/components/workspace-dock.tsx +213 -59
- package/src/hooks/internal.ts +5 -2
- package/src/hooks/use-available-models.ts +7 -2
- package/src/hooks/use-billing-usage.ts +4 -1
- package/src/hooks/use-codex-accounts.ts +74 -44
- package/src/hooks/use-composer.ts +414 -63
- package/src/hooks/use-environments.ts +92 -47
- package/src/hooks/use-file-attachments.ts +101 -55
- package/src/hooks/use-goal.ts +52 -16
- package/src/hooks/use-machine-chip.ts +134 -0
- package/src/hooks/use-machines.ts +34 -16
- package/src/hooks/use-packs.ts +24 -19
- package/src/hooks/use-relay-frame-stream.ts +5 -2
- package/src/hooks/use-rigs.ts +335 -0
- package/src/hooks/use-sandbox-files.ts +213 -39
- package/src/hooks/use-sandbox-git.ts +188 -11
- package/src/hooks/use-sandbox-terminal.ts +18 -14
- package/src/hooks/use-scheduled-tasks.ts +10 -2
- package/src/hooks/use-session-capabilities.ts +77 -20
- package/src/hooks/use-session-control.ts +50 -23
- package/src/hooks/use-session-events.ts +50 -21
- package/src/hooks/use-session-lineage.ts +128 -0
- package/src/hooks/use-session.ts +53 -30
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +202 -147
- package/src/hooks/use-windowed-sections.ts +204 -0
- package/src/hooks/use-workspace-capture.ts +233 -0
- package/src/hooks/use-workspace-edit.ts +231 -0
- package/src/hooks/use-workspace-sessions.ts +48 -5
- package/src/hooks/use-workspaces.ts +18 -15
- package/src/index.ts +133 -23
- package/src/lib/format.ts +3 -3
- package/src/lib/xterm-renderer.ts +65 -0
- package/src/lib/xterm-theme.ts +224 -18
- package/src/machines.ts +29 -3
- package/src/provider.tsx +195 -6
- package/src/timeline/activity-rail.tsx +213 -54
- package/src/timeline/disclosure-context.tsx +12 -2
- package/src/timeline/index.ts +21 -2
- package/src/timeline/parsers.ts +87 -16
- package/src/timeline/projection.ts +412 -87
- package/src/timeline/shared.tsx +111 -24
- package/src/timeline/tool-diff.tsx +24 -20
- package/src/timeline/tool-renderers.tsx +98 -21
- package/src/timeline/turn-summary.tsx +76 -23
- package/src/timeline/types.ts +96 -12
- package/styles/index.css +22 -0
- package/styles/tokens.css +2 -2
- package/dist/chunk-5TIXRCSI.js +0 -1291
- package/dist/chunk-5TIXRCSI.js.map +0 -1
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { SessionEvent, SessionStatus } from "@opengeni/sdk";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CREDIT_EXHAUSTION_MESSAGE,
|
|
4
|
+
humanizeFailureReason,
|
|
5
|
+
isCreditExhaustion,
|
|
6
|
+
tryParseJson,
|
|
7
|
+
} from "../lib/format";
|
|
3
8
|
import type {
|
|
4
9
|
AgentMessageItem,
|
|
5
10
|
ActivityItem,
|
|
11
|
+
AuthNeededItem,
|
|
6
12
|
GoalItem,
|
|
13
|
+
MemoryItem,
|
|
7
14
|
SandboxItem,
|
|
8
15
|
SessionStatusItem,
|
|
9
16
|
TimelineGroup,
|
|
@@ -30,7 +37,6 @@ import type {
|
|
|
30
37
|
/** Tool names on the first-party OpenGeni MCP server that operate on sessions. */
|
|
31
38
|
const WORKER_SPAWN_TOOL = "session_create";
|
|
32
39
|
const WORKER_MESSAGE_TOOL = "session_send_message";
|
|
33
|
-
const WORKER_INTERRUPT_TOOL = "session_interrupt";
|
|
34
40
|
|
|
35
41
|
export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
36
42
|
const items: TimelineItem[] = [];
|
|
@@ -47,9 +53,18 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
47
53
|
}
|
|
48
54
|
};
|
|
49
55
|
|
|
50
|
-
const finalizeOpen = (
|
|
56
|
+
const finalizeOpen = (
|
|
57
|
+
turnId?: string | null,
|
|
58
|
+
disposition: "complete" | "failed" | "cancelled" = "complete",
|
|
59
|
+
): void => {
|
|
51
60
|
for (const item of items) {
|
|
52
|
-
if (
|
|
61
|
+
if (
|
|
62
|
+
turnId !== undefined &&
|
|
63
|
+
"turnId" in item &&
|
|
64
|
+
item.turnId &&
|
|
65
|
+
turnId &&
|
|
66
|
+
item.turnId !== turnId
|
|
67
|
+
) {
|
|
53
68
|
continue;
|
|
54
69
|
}
|
|
55
70
|
if ((item.kind === "agent-message" || item.kind === "reasoning") && item.streaming) {
|
|
@@ -73,11 +88,27 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
73
88
|
// A steering message must not mark in-flight tools complete; it only
|
|
74
89
|
// ends whatever text was streaming. Turn lifecycle events finalize.
|
|
75
90
|
closeStreamingTail();
|
|
91
|
+
const childCompletion = workerCompletionPayload(payload.childCompletion);
|
|
92
|
+
if (childCompletion) {
|
|
93
|
+
items.push({
|
|
94
|
+
kind: "worker-completion",
|
|
95
|
+
id: event.id,
|
|
96
|
+
turnId,
|
|
97
|
+
occurredAt: event.occurredAt,
|
|
98
|
+
childSessionId: childCompletion.childSessionId,
|
|
99
|
+
childStatus: childCompletion.childStatus,
|
|
100
|
+
goalStatus: childCompletion.goalStatus,
|
|
101
|
+
goalText: childCompletion.goalText,
|
|
102
|
+
evidence: childCompletion.evidence,
|
|
103
|
+
pausedReason: childCompletion.pausedReason,
|
|
104
|
+
text: typeof payload.text === "string" ? payload.text : "",
|
|
105
|
+
});
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
76
108
|
items.push({
|
|
77
109
|
kind: "user-message",
|
|
78
110
|
id: event.id,
|
|
79
111
|
text: typeof payload.text === "string" ? payload.text : "",
|
|
80
|
-
...(event.pendingUserMessage ? { pending: true } : {}),
|
|
81
112
|
resources: resourceRefs(payload.resources),
|
|
82
113
|
tools: toolRefs(payload.tools),
|
|
83
114
|
occurredAt: event.occurredAt,
|
|
@@ -114,8 +145,14 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
114
145
|
// completed text never duplicates the streamed one.
|
|
115
146
|
const open = [...items]
|
|
116
147
|
.reverse()
|
|
117
|
-
.find(
|
|
118
|
-
|
|
148
|
+
.find(
|
|
149
|
+
(item): item is AgentMessageItem =>
|
|
150
|
+
item.kind === "agent-message" && item.turnId === turnId,
|
|
151
|
+
);
|
|
152
|
+
if (
|
|
153
|
+
open &&
|
|
154
|
+
(open.streaming || !open.text || text === open.text || text.startsWith(open.text))
|
|
155
|
+
) {
|
|
119
156
|
// The completed text is authoritative when it extends what streamed.
|
|
120
157
|
if (!open.text || (text && text.startsWith(open.text))) {
|
|
121
158
|
open.text = text || open.text;
|
|
@@ -163,23 +200,6 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
163
200
|
const callId = typeof payload.id === "string" ? payload.id : null;
|
|
164
201
|
const args = payload.arguments ?? null;
|
|
165
202
|
closeStreamingTail();
|
|
166
|
-
if (name === WORKER_INTERRUPT_TOOL) {
|
|
167
|
-
// stop (default) vs steer — the target keeps its goal on steer and
|
|
168
|
-
// picks up its next queued turn (pair with session_send_message).
|
|
169
|
-
items.push({
|
|
170
|
-
kind: "worker",
|
|
171
|
-
id: event.id,
|
|
172
|
-
turnId,
|
|
173
|
-
callId,
|
|
174
|
-
action: "interrupt",
|
|
175
|
-
prompt: null,
|
|
176
|
-
workerSessionId: extractSessionRef(args),
|
|
177
|
-
mode: workerInterruptMode(args),
|
|
178
|
-
status: "running",
|
|
179
|
-
occurredAt: event.occurredAt,
|
|
180
|
-
});
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
203
|
if (name === WORKER_SPAWN_TOOL || name === WORKER_MESSAGE_TOOL) {
|
|
184
204
|
items.push({
|
|
185
205
|
kind: "worker",
|
|
@@ -235,10 +255,37 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
235
255
|
case "sandbox.operation.completed":
|
|
236
256
|
case "sandbox.operation.failed": {
|
|
237
257
|
const name = typeof payload.name === "string" ? payload.name : "sandbox";
|
|
238
|
-
const status = event.type.endsWith(".failed")
|
|
258
|
+
const status = event.type.endsWith(".failed")
|
|
259
|
+
? "failed"
|
|
260
|
+
: event.type.endsWith(".completed")
|
|
261
|
+
? "complete"
|
|
262
|
+
: "running";
|
|
263
|
+
// Routine per-turn platform plumbing that runs before EVERY turn to
|
|
264
|
+
// guarantee box contents survive a re-warm — NOT the agent redoing work:
|
|
265
|
+
// - repository-clone: idempotent clone check + off-manifest token re-seed;
|
|
266
|
+
// - file-resource-download: idempotent `if [ ! -f ] then curl` (skips
|
|
267
|
+
// when the attached file is already on the box — see
|
|
268
|
+
// sandboxFileDownloadCommand), so an uploaded image is not re-fetched;
|
|
269
|
+
// the operation still emits every turn even when it does nothing.
|
|
270
|
+
// Rendering either every turn reads as churn. Only FAILURES surface, and
|
|
271
|
+
// they surface loudly — the failed event below creates its own item even
|
|
272
|
+
// without a started row.
|
|
273
|
+
if (
|
|
274
|
+
(name === "repository-clone" || name === "file-resource-download") &&
|
|
275
|
+
status !== "failed"
|
|
276
|
+
) {
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
239
279
|
const existing = findOpenSandbox(items, name);
|
|
240
280
|
if (existing && status !== "running") {
|
|
241
281
|
existing.status = status;
|
|
282
|
+
if (
|
|
283
|
+
payload.origin === "created" ||
|
|
284
|
+
payload.origin === "restored" ||
|
|
285
|
+
payload.origin === "resumed"
|
|
286
|
+
) {
|
|
287
|
+
existing.origin = payload.origin;
|
|
288
|
+
}
|
|
242
289
|
const message = failureMessage(payload);
|
|
243
290
|
if (message) {
|
|
244
291
|
existing.output = existing.output ? `${existing.output}\n${message}` : message;
|
|
@@ -254,6 +301,12 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
254
301
|
name,
|
|
255
302
|
command: typeof payload.command === "string" ? payload.command : null,
|
|
256
303
|
output: failureMessage(payload) ?? "",
|
|
304
|
+
origin:
|
|
305
|
+
payload.origin === "created" ||
|
|
306
|
+
payload.origin === "restored" ||
|
|
307
|
+
payload.origin === "resumed"
|
|
308
|
+
? payload.origin
|
|
309
|
+
: null,
|
|
257
310
|
status,
|
|
258
311
|
occurredAt: event.occurredAt,
|
|
259
312
|
});
|
|
@@ -263,9 +316,14 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
263
316
|
|
|
264
317
|
case "sandbox.command.output.delta": {
|
|
265
318
|
// `chunk` is the canonical wire field; text/output are legacy shapes.
|
|
266
|
-
const text =
|
|
267
|
-
|
|
268
|
-
|
|
319
|
+
const text =
|
|
320
|
+
typeof payload.chunk === "string"
|
|
321
|
+
? payload.chunk
|
|
322
|
+
: typeof payload.text === "string"
|
|
323
|
+
? payload.text
|
|
324
|
+
: typeof payload.output === "string"
|
|
325
|
+
? payload.output
|
|
326
|
+
: "";
|
|
269
327
|
if (!text) {
|
|
270
328
|
break;
|
|
271
329
|
}
|
|
@@ -273,7 +331,11 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
273
331
|
// otherwise the latest running operation is the best available owner.
|
|
274
332
|
const open =
|
|
275
333
|
(typeof payload.name === "string" ? findOpenSandbox(items, payload.name) : undefined) ??
|
|
276
|
-
[...items]
|
|
334
|
+
[...items]
|
|
335
|
+
.reverse()
|
|
336
|
+
.find(
|
|
337
|
+
(item): item is SandboxItem => item.kind === "sandbox" && item.status === "running",
|
|
338
|
+
);
|
|
277
339
|
if (open) {
|
|
278
340
|
open.output += text;
|
|
279
341
|
}
|
|
@@ -293,7 +355,9 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
293
355
|
if (!ATTENTION_STATUSES.has(status)) {
|
|
294
356
|
break;
|
|
295
357
|
}
|
|
296
|
-
const previous = [...items]
|
|
358
|
+
const previous = [...items]
|
|
359
|
+
.reverse()
|
|
360
|
+
.find((item): item is SessionStatusItem => item.kind === "session-status");
|
|
297
361
|
if (previous?.status === status) {
|
|
298
362
|
break;
|
|
299
363
|
}
|
|
@@ -313,21 +377,94 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
313
377
|
break;
|
|
314
378
|
}
|
|
315
379
|
|
|
316
|
-
case "
|
|
380
|
+
case "session.context.compacted": {
|
|
381
|
+
closeStreamingTail();
|
|
382
|
+
const before =
|
|
383
|
+
typeof payload.estimatedTokensBefore === "number"
|
|
384
|
+
? Math.round(payload.estimatedTokensBefore).toLocaleString("en-US")
|
|
385
|
+
: null;
|
|
386
|
+
const after =
|
|
387
|
+
typeof payload.estimatedTokensAfter === "number"
|
|
388
|
+
? Math.round(payload.estimatedTokensAfter).toLocaleString("en-US")
|
|
389
|
+
: null;
|
|
390
|
+
items.push({
|
|
391
|
+
kind: "notice",
|
|
392
|
+
id: event.id,
|
|
393
|
+
tone: "waiting",
|
|
394
|
+
text:
|
|
395
|
+
before && after
|
|
396
|
+
? `Context compacted from approximately ${before} to ${after} tokens.`
|
|
397
|
+
: "Context compacted so the turn could continue.",
|
|
398
|
+
occurredAt: event.occurredAt,
|
|
399
|
+
});
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
case "session.context.compaction.skipped": {
|
|
317
404
|
closeStreamingTail();
|
|
318
|
-
const
|
|
405
|
+
const reason = typeof payload.reason === "string" ? payload.reason : null;
|
|
319
406
|
items.push({
|
|
320
407
|
kind: "notice",
|
|
321
408
|
id: event.id,
|
|
322
409
|
tone: "waiting",
|
|
323
|
-
text:
|
|
324
|
-
|
|
410
|
+
text:
|
|
411
|
+
reason === "no_history"
|
|
412
|
+
? "Context compaction skipped because there is no active history to compact."
|
|
413
|
+
: reason === "replacement_not_smaller"
|
|
414
|
+
? "Context compaction skipped because the generated checkpoint would not reduce the context."
|
|
415
|
+
: "Context compaction was not needed.",
|
|
416
|
+
occurredAt: event.occurredAt,
|
|
417
|
+
});
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
case "turn.recovery.requested": {
|
|
422
|
+
// Recovery requests are durable control-plane evidence, not a user
|
|
423
|
+
// message or proof that recovery succeeded. Live state already exposes
|
|
424
|
+
// a genuinely recovering session; keep the raw event in Debug/audit.
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
case "turn.event.rejected_late": {
|
|
429
|
+
// Attempt-fence rejections prove stale callbacks did not alter current
|
|
430
|
+
// truth. They are useful diagnostics but never actionable chat content,
|
|
431
|
+
// regardless of the rejected event type. Debug/audit retains the event.
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
case "tool.auth_needed": {
|
|
436
|
+
// Keep the whole structured payload — the renderer turns it into a clean
|
|
437
|
+
// inline reconnect card, and the app starts the recovery flow off the
|
|
438
|
+
// connectionId/resource. Losing it to a plain-text notice was the ugly
|
|
439
|
+
// "linear.app needs to be reconnected." line users complained about.
|
|
440
|
+
closeStreamingTail();
|
|
441
|
+
items.push({
|
|
442
|
+
kind: "auth-needed",
|
|
443
|
+
id: event.id,
|
|
444
|
+
turnId,
|
|
445
|
+
providerDomain: typeof payload.providerDomain === "string" ? payload.providerDomain : "",
|
|
446
|
+
connectionId: typeof payload.connectionId === "string" ? payload.connectionId : null,
|
|
447
|
+
reason: authNeededReason(payload.reason),
|
|
448
|
+
scopes: stringList(payload.scopes),
|
|
449
|
+
resource: typeof payload.resource === "string" ? payload.resource : null,
|
|
450
|
+
toolName: typeof payload.toolName === "string" ? payload.toolName : null,
|
|
451
|
+
authorizationUrl:
|
|
452
|
+
typeof payload.authorizationUrl === "string" ? payload.authorizationUrl : null,
|
|
325
453
|
occurredAt: event.occurredAt,
|
|
326
454
|
});
|
|
327
455
|
break;
|
|
328
456
|
}
|
|
329
457
|
|
|
330
458
|
case "turn.completed": {
|
|
459
|
+
// A standalone manual compaction uses the turn ledger for fencing and
|
|
460
|
+
// recovery, but it is maintenance rather than a conversational turn.
|
|
461
|
+
// The dedicated session.context.compacted notice is the complete UI
|
|
462
|
+
// truth; adding a generic turn chip would falsely make it look like an
|
|
463
|
+
// extra agent response.
|
|
464
|
+
if (payload.maintenance === "context_compaction") {
|
|
465
|
+
finalizeOpen(turnId);
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
331
468
|
// Credit exhaustion arrives as a NOMINALLY completed turn (`detail:
|
|
332
469
|
// "insufficient OpenGeni credits"`, `segmentLimit: "budget_exhausted"`)
|
|
333
470
|
// — the engine ended the segment early, it did not finish the work.
|
|
@@ -356,7 +493,9 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
356
493
|
// Credit death can hide behind fields `failureMessage` doesn't read
|
|
357
494
|
// (detail/segmentLimit), so classify the whole payload before falling
|
|
358
495
|
// back to the generic error/message extraction.
|
|
359
|
-
const failureText = isCreditExhaustionPayload(payload)
|
|
496
|
+
const failureText = isCreditExhaustionPayload(payload)
|
|
497
|
+
? CREDIT_EXHAUSTION_MESSAGE
|
|
498
|
+
: failureMessage(payload);
|
|
360
499
|
// The TURN failed — the in-flight items did not. Chip doctrine: red is
|
|
361
500
|
// spent once, on the turn-level outcome. Items caught mid-flight read
|
|
362
501
|
// as calm "interrupted" (same as turn.cancelled); an item that itself
|
|
@@ -397,11 +536,25 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
397
536
|
break;
|
|
398
537
|
}
|
|
399
538
|
|
|
539
|
+
case "memory.saved":
|
|
540
|
+
case "memory.corrected": {
|
|
541
|
+
// A first-party memory write is a discrete step, not streamed text, so it
|
|
542
|
+
// ends whatever was streaming (mirrors tool/sandbox pushes). A payload
|
|
543
|
+
// missing the memory id is malformed and dropped rather than shown blank.
|
|
544
|
+
const memory = memoryItem(event.id, event.type, turnId, payload, event.occurredAt);
|
|
545
|
+
if (memory) {
|
|
546
|
+
closeStreamingTail();
|
|
547
|
+
items.push(memory);
|
|
548
|
+
}
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
|
|
400
552
|
case "goal.set":
|
|
401
553
|
case "goal.updated":
|
|
402
554
|
case "goal.completed":
|
|
403
555
|
case "goal.paused":
|
|
404
556
|
case "goal.resumed":
|
|
557
|
+
case "goal.cleared":
|
|
405
558
|
case "goal.continuation": {
|
|
406
559
|
items.push({
|
|
407
560
|
kind: "goal",
|
|
@@ -440,7 +593,11 @@ export function creditExhaustedFromEvents(events: SessionEvent[]): boolean {
|
|
|
440
593
|
const ordered = [...events].sort((a, b) => a.sequence - b.sequence);
|
|
441
594
|
for (let index = ordered.length - 1; index >= 0; index -= 1) {
|
|
442
595
|
const event = ordered[index];
|
|
443
|
-
if (
|
|
596
|
+
if (
|
|
597
|
+
event?.type !== "turn.completed" &&
|
|
598
|
+
event?.type !== "turn.failed" &&
|
|
599
|
+
event?.type !== "turn.cancelled"
|
|
600
|
+
) {
|
|
444
601
|
continue;
|
|
445
602
|
}
|
|
446
603
|
return isCreditExhaustionPayload(asRecord(event.payload));
|
|
@@ -480,6 +637,7 @@ function isActivityItem(item: TimelineItem): item is ActivityItem {
|
|
|
480
637
|
case "tool-call":
|
|
481
638
|
case "worker":
|
|
482
639
|
case "sandbox":
|
|
640
|
+
case "memory":
|
|
483
641
|
return true;
|
|
484
642
|
default:
|
|
485
643
|
return false;
|
|
@@ -510,8 +668,6 @@ export function groupTimeline(items: TimelineItem[]): TimelineGroup[] {
|
|
|
510
668
|
|
|
511
669
|
/* --- helpers ---------------------------------------------------------------- */
|
|
512
670
|
|
|
513
|
-
type TimelineEvent = SessionEvent & { pendingUserMessage?: true };
|
|
514
|
-
|
|
515
671
|
type TurnAnchorPrescan = {
|
|
516
672
|
queuedTurnByTrigger: Map<string, string>;
|
|
517
673
|
startSeqByTrigger: Map<string, number>;
|
|
@@ -531,7 +687,8 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
|
|
|
531
687
|
const payload = asRecord(event.payload);
|
|
532
688
|
const turnId = event.turnId ?? null;
|
|
533
689
|
if (event.type === "turn.queued") {
|
|
534
|
-
const triggerEventId =
|
|
690
|
+
const triggerEventId =
|
|
691
|
+
typeof payload.triggerEventId === "string" ? payload.triggerEventId : null;
|
|
535
692
|
const queuedTurnId = typeof payload.turnId === "string" ? payload.turnId : turnId;
|
|
536
693
|
if (triggerEventId && queuedTurnId) {
|
|
537
694
|
queuedTurnByTrigger.set(triggerEventId, queuedTurnId);
|
|
@@ -539,8 +696,9 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
|
|
|
539
696
|
continue;
|
|
540
697
|
}
|
|
541
698
|
if (event.type === "turn.started") {
|
|
542
|
-
const triggerEventId =
|
|
543
|
-
|
|
699
|
+
const triggerEventId =
|
|
700
|
+
typeof payload.triggerEventId === "string" ? payload.triggerEventId : null;
|
|
701
|
+
if (triggerEventId && !startSeqByTrigger.has(triggerEventId)) {
|
|
544
702
|
startSeqByTrigger.set(triggerEventId, event.sequence);
|
|
545
703
|
}
|
|
546
704
|
if (turnId) {
|
|
@@ -550,13 +708,13 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
|
|
|
550
708
|
cancelledTurnIds.add(turnId);
|
|
551
709
|
}
|
|
552
710
|
|
|
553
|
-
if (turnId && event.type
|
|
711
|
+
if (turnId && isTurnExecutionEvidence(event.type)) {
|
|
554
712
|
const previous = fallbackSeqByTurn.get(turnId);
|
|
555
713
|
if (previous === undefined || event.sequence < previous) {
|
|
556
714
|
fallbackSeqByTurn.set(turnId, event.sequence);
|
|
557
715
|
}
|
|
558
716
|
}
|
|
559
|
-
if (turnId &&
|
|
717
|
+
if (turnId && isTurnExecutionEvidence(event.type)) {
|
|
560
718
|
startedTurnIds.add(turnId);
|
|
561
719
|
}
|
|
562
720
|
}
|
|
@@ -572,7 +730,11 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
|
|
|
572
730
|
|
|
573
731
|
const cancelledBeforeStartTriggers = new Set<string>();
|
|
574
732
|
for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
|
|
575
|
-
if (
|
|
733
|
+
if (
|
|
734
|
+
cancelledTurnIds.has(turnId) &&
|
|
735
|
+
!startSeqByTrigger.has(triggerEventId) &&
|
|
736
|
+
!fallbackSeqByTurn.has(turnId)
|
|
737
|
+
) {
|
|
576
738
|
cancelledBeforeStartTriggers.add(triggerEventId);
|
|
577
739
|
}
|
|
578
740
|
}
|
|
@@ -580,10 +742,9 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
|
|
|
580
742
|
return { queuedTurnByTrigger, startSeqByTrigger, cancelledBeforeStartTriggers, startedTurnIds };
|
|
581
743
|
}
|
|
582
744
|
|
|
583
|
-
function orderTimelineEvents(events: SessionEvent[], prescan: TurnAnchorPrescan):
|
|
745
|
+
function orderTimelineEvents(events: SessionEvent[], prescan: TurnAnchorPrescan): SessionEvent[] {
|
|
584
746
|
const ordered = [...events].sort((a, b) => a.sequence - b.sequence);
|
|
585
|
-
const insertions = new Map<number,
|
|
586
|
-
const pending: TimelineEvent[] = [];
|
|
747
|
+
const insertions = new Map<number, SessionEvent[]>();
|
|
587
748
|
|
|
588
749
|
for (const event of ordered) {
|
|
589
750
|
if (event.type !== "user.message") {
|
|
@@ -602,10 +763,12 @@ function orderTimelineEvents(events: SessionEvent[], prescan: TurnAnchorPrescan)
|
|
|
602
763
|
pushInsertion(insertions, startSeq, event);
|
|
603
764
|
continue;
|
|
604
765
|
}
|
|
605
|
-
|
|
766
|
+
// A waiting prompt belongs only in the prompt queue. It enters the
|
|
767
|
+
// timeline at turn.started (or the first same-turn activity fallback),
|
|
768
|
+
// never as a second queued representation.
|
|
606
769
|
}
|
|
607
770
|
|
|
608
|
-
const projected:
|
|
771
|
+
const projected: SessionEvent[] = [];
|
|
609
772
|
for (const event of ordered) {
|
|
610
773
|
const before = insertions.get(event.sequence);
|
|
611
774
|
if (before) {
|
|
@@ -615,11 +778,14 @@ function orderTimelineEvents(events: SessionEvent[], prescan: TurnAnchorPrescan)
|
|
|
615
778
|
projected.push(event);
|
|
616
779
|
}
|
|
617
780
|
}
|
|
618
|
-
projected.push(...pending);
|
|
619
781
|
return projected;
|
|
620
782
|
}
|
|
621
783
|
|
|
622
|
-
function pushInsertion(
|
|
784
|
+
function pushInsertion(
|
|
785
|
+
insertions: Map<number, SessionEvent[]>,
|
|
786
|
+
sequence: number,
|
|
787
|
+
event: SessionEvent,
|
|
788
|
+
): void {
|
|
623
789
|
const bucket = insertions.get(sequence);
|
|
624
790
|
if (bucket) {
|
|
625
791
|
bucket.push(event);
|
|
@@ -632,6 +798,28 @@ function isAgentActivityEvent(type: string): boolean {
|
|
|
632
798
|
return type.startsWith("agent.") || type.startsWith("sandbox.");
|
|
633
799
|
}
|
|
634
800
|
|
|
801
|
+
/**
|
|
802
|
+
* Evidence that a queued prompt crossed the execution boundary when an older
|
|
803
|
+
* or partially recovered ledger is missing its canonical `turn.started`.
|
|
804
|
+
* Queue/control bookkeeping intentionally does not qualify: moving, editing,
|
|
805
|
+
* steering, or deleting a waiting row must never make its prompt appear in the
|
|
806
|
+
* transcript as though inference had begun.
|
|
807
|
+
*/
|
|
808
|
+
function isTurnExecutionEvidence(type: string): boolean {
|
|
809
|
+
return (
|
|
810
|
+
isAgentActivityEvent(type) ||
|
|
811
|
+
type === "turn.completed" ||
|
|
812
|
+
type === "turn.failed" ||
|
|
813
|
+
type === "turn.recovery.requested" ||
|
|
814
|
+
type === "turn.capacity_waiting" ||
|
|
815
|
+
type === "session.requiresAction" ||
|
|
816
|
+
type === "tool.auth_needed" ||
|
|
817
|
+
type.startsWith("rig.setup.") ||
|
|
818
|
+
type === "codex.capacity.waiting" ||
|
|
819
|
+
type === "codex.capacity.resumed"
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
635
823
|
function turnEndItem(
|
|
636
824
|
event: SessionEvent,
|
|
637
825
|
outcome: TurnEndItem["outcome"],
|
|
@@ -681,7 +869,10 @@ function stampTurnOutcome(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
|
|
|
681
869
|
}
|
|
682
870
|
}
|
|
683
871
|
|
|
684
|
-
function applyTurnOutcome(
|
|
872
|
+
function applyTurnOutcome(
|
|
873
|
+
group: Extract<TimelineGroup, { kind: "activity" }>,
|
|
874
|
+
turnEnd: TurnEndItem,
|
|
875
|
+
): void {
|
|
685
876
|
// A sub-cluster reports ITS OWN outcome, not the turn's. When a turn fails
|
|
686
877
|
// at step 7, clusters 1–6 completed — painting them all red says "everything
|
|
687
878
|
// broke" when one thing did. The turn-level fold carries the turn outcome;
|
|
@@ -692,7 +883,9 @@ function applyTurnOutcome(group: Extract<TimelineGroup, { kind: "activity" }>, t
|
|
|
692
883
|
return;
|
|
693
884
|
}
|
|
694
885
|
const hasFailed = group.items.some((item) => "status" in item && item.status === "failed");
|
|
695
|
-
const hasInterrupted = group.items.some(
|
|
886
|
+
const hasInterrupted = group.items.some(
|
|
887
|
+
(item) => "status" in item && item.status === "cancelled",
|
|
888
|
+
);
|
|
696
889
|
group.outcome = hasFailed ? "failed" : hasInterrupted ? "cancelled" : "complete";
|
|
697
890
|
if (turnEnd.failureText && hasFailed) {
|
|
698
891
|
group.failureText = turnEnd.failureText;
|
|
@@ -743,7 +936,11 @@ function foldSettledTurn(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
|
|
|
743
936
|
turnGroup.failureText = turnEnd.failureText;
|
|
744
937
|
}
|
|
745
938
|
|
|
746
|
-
groups.splice(
|
|
939
|
+
groups.splice(
|
|
940
|
+
startIndex,
|
|
941
|
+
collected.length,
|
|
942
|
+
...(finalMessage ? [turnGroup, finalMessage] : [turnGroup]),
|
|
943
|
+
);
|
|
747
944
|
}
|
|
748
945
|
|
|
749
946
|
function isTurnBoundary(group: TimelineGroup | undefined): boolean {
|
|
@@ -755,16 +952,31 @@ function belongsToDifferentTurn(group: TimelineGroup | undefined, turnId: string
|
|
|
755
952
|
return false;
|
|
756
953
|
}
|
|
757
954
|
if (group.kind === "activity") {
|
|
758
|
-
return
|
|
955
|
+
return (
|
|
956
|
+
group.items.length > 0 &&
|
|
957
|
+
group.items.every((item) => item.turnId !== null && item.turnId !== turnId)
|
|
958
|
+
);
|
|
759
959
|
}
|
|
760
|
-
return
|
|
960
|
+
return (
|
|
961
|
+
group.kind === "item" &&
|
|
962
|
+
group.item.kind === "agent-message" &&
|
|
963
|
+
group.item.turnId !== null &&
|
|
964
|
+
group.item.turnId !== turnId
|
|
965
|
+
);
|
|
761
966
|
}
|
|
762
967
|
|
|
763
968
|
function isBetweenTurnDivider(group: TimelineGroup | undefined): boolean {
|
|
764
|
-
return
|
|
969
|
+
return (
|
|
970
|
+
group?.kind === "item" &&
|
|
971
|
+
group.item.kind === "session-status" &&
|
|
972
|
+
group.item.status !== "running"
|
|
973
|
+
);
|
|
765
974
|
}
|
|
766
975
|
|
|
767
|
-
function extractFinalAgentMessage(
|
|
976
|
+
function extractFinalAgentMessage(
|
|
977
|
+
groups: TimelineGroup[],
|
|
978
|
+
turnEnd: TurnEndItem,
|
|
979
|
+
): Extract<TimelineGroup, { kind: "item" }> | null {
|
|
768
980
|
const tail = groups[groups.length - 1];
|
|
769
981
|
if (tail?.kind !== "item" || tail.item.kind !== "agent-message" || tail.item.streaming) {
|
|
770
982
|
return null;
|
|
@@ -793,10 +1005,21 @@ function asRecord(value: unknown): Record<string, unknown> {
|
|
|
793
1005
|
return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
794
1006
|
}
|
|
795
1007
|
|
|
796
|
-
const SESSION_STATUSES: readonly SessionStatus[] = [
|
|
1008
|
+
const SESSION_STATUSES: readonly SessionStatus[] = [
|
|
1009
|
+
"queued",
|
|
1010
|
+
"running",
|
|
1011
|
+
"idle",
|
|
1012
|
+
"requires_action",
|
|
1013
|
+
"failed",
|
|
1014
|
+
"cancelled",
|
|
1015
|
+
];
|
|
797
1016
|
|
|
798
1017
|
/** Statuses that demand the reader's attention and so earn a timeline divider. */
|
|
799
|
-
const ATTENTION_STATUSES: ReadonlySet<SessionStatus> = new Set([
|
|
1018
|
+
const ATTENTION_STATUSES: ReadonlySet<SessionStatus> = new Set([
|
|
1019
|
+
"requires_action",
|
|
1020
|
+
"failed",
|
|
1021
|
+
"cancelled",
|
|
1022
|
+
]);
|
|
800
1023
|
|
|
801
1024
|
/** Keep only entries that match the wire shapes; user payloads are untyped. */
|
|
802
1025
|
function resourceRefs(value: unknown): import("@opengeni/sdk").ResourceRef[] {
|
|
@@ -822,6 +1045,43 @@ function toolRefs(value: unknown): import("@opengeni/sdk").ToolRef[] {
|
|
|
822
1045
|
});
|
|
823
1046
|
}
|
|
824
1047
|
|
|
1048
|
+
function workerCompletionPayload(value: unknown): {
|
|
1049
|
+
childSessionId: string;
|
|
1050
|
+
childStatus: string;
|
|
1051
|
+
goalStatus: string | null;
|
|
1052
|
+
goalText: string | null;
|
|
1053
|
+
evidence: string | null;
|
|
1054
|
+
pausedReason: string | null;
|
|
1055
|
+
} | null {
|
|
1056
|
+
const payload = asRecord(value);
|
|
1057
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1058
|
+
if (
|
|
1059
|
+
typeof payload.childSessionId !== "string" ||
|
|
1060
|
+
!uuidPattern.test(payload.childSessionId) ||
|
|
1061
|
+
typeof payload.status !== "string" ||
|
|
1062
|
+
payload.status.trim() === ""
|
|
1063
|
+
) {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
const goal = asRecord(payload.goal);
|
|
1067
|
+
return {
|
|
1068
|
+
childSessionId: payload.childSessionId,
|
|
1069
|
+
childStatus: payload.status,
|
|
1070
|
+
goalStatus: typeof goal.status === "string" ? goal.status : null,
|
|
1071
|
+
goalText: typeof goal.text === "string" ? goal.text : null,
|
|
1072
|
+
evidence: typeof goal.evidence === "string" ? goal.evidence : null,
|
|
1073
|
+
// Console pauses and several server paths put the human explanation on
|
|
1074
|
+
// `rationale`, not `pausedReason` — same fallback the goal pill uses, so a
|
|
1075
|
+
// paused worker card never shows "Worker paused" with the reason missing.
|
|
1076
|
+
pausedReason:
|
|
1077
|
+
typeof goal.pausedReason === "string"
|
|
1078
|
+
? goal.pausedReason
|
|
1079
|
+
: typeof goal.rationale === "string"
|
|
1080
|
+
? goal.rationale
|
|
1081
|
+
: null,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
825
1085
|
function isSessionStatus(value: unknown): value is SessionStatus {
|
|
826
1086
|
return typeof value === "string" && (SESSION_STATUSES as readonly string[]).includes(value);
|
|
827
1087
|
}
|
|
@@ -832,23 +1092,36 @@ function isErrorOutput(payload: Record<string, unknown>): boolean {
|
|
|
832
1092
|
return true;
|
|
833
1093
|
}
|
|
834
1094
|
const output = payload.output;
|
|
835
|
-
return
|
|
1095
|
+
return (
|
|
1096
|
+
!!output && typeof output === "object" && (output as { isError?: unknown }).isError === true
|
|
1097
|
+
);
|
|
836
1098
|
}
|
|
837
1099
|
|
|
838
|
-
function findOpenCall(
|
|
1100
|
+
function findOpenCall(
|
|
1101
|
+
items: TimelineItem[],
|
|
1102
|
+
callId: string | null,
|
|
1103
|
+
): ToolCallItem | WorkerItem | undefined {
|
|
839
1104
|
const reversed = [...items].reverse();
|
|
840
|
-
const isCall = (item: TimelineItem): item is ToolCallItem | WorkerItem =>
|
|
1105
|
+
const isCall = (item: TimelineItem): item is ToolCallItem | WorkerItem =>
|
|
1106
|
+
item.kind === "tool-call" || item.kind === "worker";
|
|
841
1107
|
if (callId) {
|
|
842
1108
|
const byId = reversed.find((item) => isCall(item) && item.callId === callId);
|
|
843
1109
|
if (byId) {
|
|
844
1110
|
return byId as ToolCallItem | WorkerItem;
|
|
845
1111
|
}
|
|
846
1112
|
}
|
|
847
|
-
return reversed.find(
|
|
1113
|
+
return reversed.find(
|
|
1114
|
+
(item): item is ToolCallItem | WorkerItem => isCall(item) && item.status === "running",
|
|
1115
|
+
);
|
|
848
1116
|
}
|
|
849
1117
|
|
|
850
1118
|
function findOpenSandbox(items: TimelineItem[], name: string): SandboxItem | undefined {
|
|
851
|
-
return [...items]
|
|
1119
|
+
return [...items]
|
|
1120
|
+
.reverse()
|
|
1121
|
+
.find(
|
|
1122
|
+
(item): item is SandboxItem =>
|
|
1123
|
+
item.kind === "sandbox" && item.name === name && item.status === "running",
|
|
1124
|
+
);
|
|
852
1125
|
}
|
|
853
1126
|
|
|
854
1127
|
function failureMessage(payload: Record<string, unknown>): string | null {
|
|
@@ -877,20 +1150,61 @@ function goalText(payload: Record<string, unknown>): string | null {
|
|
|
877
1150
|
return null;
|
|
878
1151
|
}
|
|
879
1152
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1153
|
+
/**
|
|
1154
|
+
* Fold a `memory.saved` / `memory.corrected` event into a {@link MemoryItem}.
|
|
1155
|
+
* Reads DEFENSIVELY (the payload is untyped `unknown`, no Zod schema): a missing
|
|
1156
|
+
* memory id means a malformed event, so we return null and the case drops it.
|
|
1157
|
+
*/
|
|
1158
|
+
function memoryItem(
|
|
1159
|
+
id: string,
|
|
1160
|
+
type: string,
|
|
1161
|
+
turnId: string | null,
|
|
1162
|
+
payload: Record<string, unknown>,
|
|
1163
|
+
occurredAt: string,
|
|
1164
|
+
): MemoryItem | null {
|
|
1165
|
+
const memoryId =
|
|
1166
|
+
typeof payload.memoryId === "string" && payload.memoryId ? payload.memoryId : null;
|
|
1167
|
+
if (!memoryId) {
|
|
1168
|
+
return null;
|
|
892
1169
|
}
|
|
893
|
-
|
|
1170
|
+
const replacementPreview =
|
|
1171
|
+
typeof payload.replacementPreview === "string" ? payload.replacementPreview : undefined;
|
|
1172
|
+
const replacementMemoryId =
|
|
1173
|
+
typeof payload.replacementMemoryId === "string" ? payload.replacementMemoryId : undefined;
|
|
1174
|
+
const action = typeof payload.action === "string" ? payload.action : undefined;
|
|
1175
|
+
return {
|
|
1176
|
+
kind: "memory",
|
|
1177
|
+
id,
|
|
1178
|
+
turnId,
|
|
1179
|
+
variant: type === "memory.corrected" ? "corrected" : "saved",
|
|
1180
|
+
memoryKind: typeof payload.kind === "string" ? payload.kind : "",
|
|
1181
|
+
preview: typeof payload.preview === "string" ? payload.preview : "",
|
|
1182
|
+
...(payload.deduped === true ? { deduped: true } : {}),
|
|
1183
|
+
...(replacementPreview ? { replacementPreview } : {}),
|
|
1184
|
+
...(action ? { action } : {}),
|
|
1185
|
+
memoryId,
|
|
1186
|
+
...(replacementMemoryId ? { replacementMemoryId } : {}),
|
|
1187
|
+
occurredAt,
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const AUTH_NEEDED_REASONS: ReadonlySet<string> = new Set([
|
|
1192
|
+
"missing_connection",
|
|
1193
|
+
"expired",
|
|
1194
|
+
"insufficient_scope",
|
|
1195
|
+
"refresh_failed",
|
|
1196
|
+
]);
|
|
1197
|
+
|
|
1198
|
+
function authNeededReason(value: unknown): AuthNeededItem["reason"] {
|
|
1199
|
+
return typeof value === "string" && AUTH_NEEDED_REASONS.has(value)
|
|
1200
|
+
? (value as AuthNeededItem["reason"])
|
|
1201
|
+
: null;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function stringList(value: unknown): string[] {
|
|
1205
|
+
return Array.isArray(value)
|
|
1206
|
+
? value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
|
|
1207
|
+
: [];
|
|
894
1208
|
}
|
|
895
1209
|
|
|
896
1210
|
function reasoningText(payload: unknown): string {
|
|
@@ -922,12 +1236,6 @@ function workerPrompt(args: unknown): string | null {
|
|
|
922
1236
|
return null;
|
|
923
1237
|
}
|
|
924
1238
|
|
|
925
|
-
/** The interrupt mode from `session_interrupt` args; defaults to "stop". */
|
|
926
|
-
function workerInterruptMode(args: unknown): "stop" | "steer" {
|
|
927
|
-
const record = asRecord(typeof args === "string" ? tryParseJson(args) : args);
|
|
928
|
-
return record.mode === "steer" ? "steer" : "stop";
|
|
929
|
-
}
|
|
930
|
-
|
|
931
1239
|
/**
|
|
932
1240
|
* Find a session id in orchestration tool arguments or output. Handles raw
|
|
933
1241
|
* objects, JSON strings, and MCP tool results (`{ content: [{ type: "text",
|
|
@@ -956,7 +1264,11 @@ export function extractSessionRef(value: unknown, depth = 0): string | null {
|
|
|
956
1264
|
if (typeof record.sessionId === "string" && looksLikeId(record.sessionId)) {
|
|
957
1265
|
return record.sessionId;
|
|
958
1266
|
}
|
|
959
|
-
if (
|
|
1267
|
+
if (
|
|
1268
|
+
typeof record.id === "string" &&
|
|
1269
|
+
looksLikeId(record.id) &&
|
|
1270
|
+
("status" in record || "workspaceId" in record || "initialMessage" in record)
|
|
1271
|
+
) {
|
|
960
1272
|
return record.id;
|
|
961
1273
|
}
|
|
962
1274
|
for (const key of ["structuredContent", "session", "result", "content"] as const) {
|
|
@@ -977,7 +1289,20 @@ function looksLikeId(value: string): boolean {
|
|
|
977
1289
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
978
1290
|
}
|
|
979
1291
|
|
|
980
|
-
/**
|
|
1292
|
+
/**
|
|
1293
|
+
* Readable label for a tool call ("session_create" -> "session create").
|
|
1294
|
+
*
|
|
1295
|
+
* MCP tools are namespaced `<serverId>__<toolName>` (see prefixedMcpToolName),
|
|
1296
|
+
* and for catalog-imported servers that serverId is an opaque slug+hash
|
|
1297
|
+
* ("mcp-integrations-sh-supabase-com-34ed9dcf1390-0i6tcf8"). De-slugging the
|
|
1298
|
+
* whole thing leaked that id into the timeline; strip the server prefix and show
|
|
1299
|
+
* just the tool ("list organizations"). Names without the `__` boundary (plain
|
|
1300
|
+
* built-ins like "session_create") are unaffected.
|
|
1301
|
+
*/
|
|
981
1302
|
export function toolDisplayName(name: string): string {
|
|
982
|
-
|
|
1303
|
+
// The prefix is a single LEFT boundary (`registryId__toolName`), so split on
|
|
1304
|
+
// the FIRST `__` — the tool name itself may contain `__` and must survive whole.
|
|
1305
|
+
const boundary = name.indexOf("__");
|
|
1306
|
+
const toolPart = boundary >= 0 ? name.slice(boundary + 2) : name;
|
|
1307
|
+
return toolPart.replace(/[_-]+/g, " ").trim();
|
|
983
1308
|
}
|