@opengeni/react 3.5.1-canary.0 → 3.7.0-canary.1

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 (87) hide show
  1. package/dist/artifacts.d.ts +1 -1
  2. package/dist/artifacts.js +248 -6
  3. package/dist/artifacts.js.map +1 -1
  4. package/dist/{browser-viewer-E5DDEFQ4.js → browser-viewer-NTD6UIPR.js} +3 -3
  5. package/dist/{chunk-W2RGYBCV.js → chunk-24M4YBCL.js} +4 -4
  6. package/dist/{chunk-WY3MELVJ.js → chunk-5IXTJTK3.js} +1438 -397
  7. package/dist/chunk-5IXTJTK3.js.map +1 -0
  8. package/dist/chunk-5PQJOCX5.js +902 -0
  9. package/dist/chunk-5PQJOCX5.js.map +1 -0
  10. package/dist/{chunk-UNCXXIQR.js → chunk-INGB2N5R.js} +4310 -3982
  11. package/dist/chunk-INGB2N5R.js.map +1 -0
  12. package/dist/{chunk-TJFMMDQU.js → chunk-MOWYEBCQ.js} +1839 -17
  13. package/dist/chunk-MOWYEBCQ.js.map +1 -0
  14. package/dist/chunk-PAJU5KKL.js +331 -0
  15. package/dist/chunk-PAJU5KKL.js.map +1 -0
  16. package/dist/{chunk-VTOREEXF.js → chunk-PLYRL5ER.js} +24 -2
  17. package/dist/{chunk-VTOREEXF.js.map → chunk-PLYRL5ER.js.map} +1 -1
  18. package/dist/{chunk-YVCHAQZY.js → chunk-ZFOFY5ST.js} +27 -349
  19. package/dist/chunk-ZFOFY5ST.js.map +1 -0
  20. package/dist/client.d.ts +1 -1
  21. package/dist/components/artifacts/index.d.ts +1 -1
  22. package/dist/components/artifacts/published-html-artifact-frame.d.ts +46 -3
  23. package/dist/components/composer.d.ts +2 -0
  24. package/dist/components/session-chrome.d.ts +38 -5
  25. package/dist/components/session-commands-panel.d.ts +6 -0
  26. package/dist/components/session-conversation.d.ts +14 -0
  27. package/dist/components/timeline-anchor.d.ts +21 -0
  28. package/dist/composer.js +2 -3
  29. package/dist/conversation-timeline.d.ts +5 -0
  30. package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-YZ3ZKWMM.js} +2 -1
  31. package/dist/fleet-decision-row-YZ3ZKWMM.js.map +1 -0
  32. package/dist/hooks/use-file-attachments.d.ts +6 -0
  33. package/dist/hooks/use-sandbox-files.d.ts +4 -1
  34. package/dist/hooks/use-workspace-edit.d.ts +4 -0
  35. package/dist/index.d.ts +4 -1
  36. package/dist/index.js +823 -965
  37. package/dist/index.js.map +1 -1
  38. package/dist/interaction.js +2 -2
  39. package/dist/lib/format.d.ts +5 -0
  40. package/dist/machines.js +3 -2
  41. package/dist/session-ui.d.ts +3 -0
  42. package/dist/session-ui.js +14 -4
  43. package/dist/session.js +19 -19
  44. package/dist/timeline/registry.d.ts +2 -0
  45. package/dist/timeline/types.d.ts +4 -2
  46. package/package.json +2 -2
  47. package/src/artifacts.ts +3 -0
  48. package/src/client.ts +2 -1
  49. package/src/components/artifacts/index.ts +3 -0
  50. package/src/components/artifacts/published-html-artifact-frame.tsx +307 -4
  51. package/src/components/composer.tsx +89 -25
  52. package/src/components/machine-input-display.ts +6 -0
  53. package/src/components/message-timeline.tsx +351 -204
  54. package/src/components/sandbox-workspace.tsx +11 -1
  55. package/src/components/session-chrome.tsx +501 -128
  56. package/src/components/session-commands-panel.tsx +113 -0
  57. package/src/components/session-conversation.tsx +141 -0
  58. package/src/components/timeline-anchor.tsx +91 -0
  59. package/src/conversation-timeline.ts +83 -0
  60. package/src/hooks/use-file-attachments.ts +34 -0
  61. package/src/hooks/use-sandbox-files.ts +68 -20
  62. package/src/hooks/use-session-background-commands.ts +21 -10
  63. package/src/hooks/use-session-events.ts +56 -39
  64. package/src/hooks/use-session.ts +23 -2
  65. package/src/hooks/use-workspace-edit.ts +23 -5
  66. package/src/index.ts +4 -0
  67. package/src/lib/format.ts +32 -0
  68. package/src/session-ui.ts +3 -0
  69. package/src/timeline/fleet-decision-projection.ts +2 -1
  70. package/src/timeline/fleet-decision-row.tsx +1 -0
  71. package/src/timeline/projection.ts +157 -19
  72. package/src/timeline/registry.ts +15 -3
  73. package/src/timeline/tool-renderers.tsx +117 -1
  74. package/src/timeline/types.ts +12 -1
  75. package/styles/compiled.css +495 -411
  76. package/styles/index.css +23 -0
  77. package/dist/chunk-7EAFGICR.js +0 -302
  78. package/dist/chunk-7EAFGICR.js.map +0 -1
  79. package/dist/chunk-IB27C53Y.js +0 -2582
  80. package/dist/chunk-IB27C53Y.js.map +0 -1
  81. package/dist/chunk-TJFMMDQU.js.map +0 -1
  82. package/dist/chunk-UNCXXIQR.js.map +0 -1
  83. package/dist/chunk-WY3MELVJ.js.map +0 -1
  84. package/dist/chunk-YVCHAQZY.js.map +0 -1
  85. package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
  86. /package/dist/{browser-viewer-E5DDEFQ4.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
  87. /package/dist/{chunk-W2RGYBCV.js.map → chunk-24M4YBCL.js.map} +0 -0
package/src/index.ts CHANGED
@@ -462,6 +462,8 @@ export type { CommandPaletteProps } from "./components/command-palette";
462
462
 
463
463
  // Components
464
464
  export { ChatComposer } from "./components/chat-composer";
465
+ export { SessionConversation } from "./components/session-conversation";
466
+ export type { SessionConversationProps } from "./components/session-conversation";
465
467
  export type { ChatComposerProps } from "./components/chat-composer";
466
468
  export { ComposerTranscriptionControl } from "./components/composer-transcription-control";
467
469
  export type {
@@ -600,8 +602,10 @@ export {
600
602
  formatClockTime,
601
603
  formatRelativeTime,
602
604
  humanizeFailureReason,
605
+ presentFailure,
603
606
  isCreditExhaustion,
604
607
  stringifyPayload,
605
608
  truncate,
606
609
  tryParseJson,
607
610
  } from "./lib/format";
611
+ export { SessionCommandsPanel } from "./components/session-commands-panel";
package/src/lib/format.ts CHANGED
@@ -186,3 +186,35 @@ export function humanizeFailureReason(reason: string | null): string | null {
186
186
  }
187
187
  return reason;
188
188
  }
189
+
190
+ /** Project failure diagnostics without changing the stored event, including legacy retry wrappers. */
191
+ export function presentFailure(payload: Record<string, unknown>): {
192
+ reason: string | null;
193
+ safetyRefusal: boolean;
194
+ } {
195
+ const text = (key: string): string | null => {
196
+ const value = payload[key];
197
+ return typeof value === "string" && value.trim() ? value : null;
198
+ };
199
+ const message = text("error") ?? text("message");
200
+ const detail = text("lastRetryableError") ?? text("detail");
201
+ const safetyRefusal =
202
+ payload.code === "provider_safety_refusal" ||
203
+ [message, detail].some(
204
+ (value) =>
205
+ value !== null && /\bthis request was blocked by our safety systems\b/i.test(value),
206
+ );
207
+ if (safetyRefusal) {
208
+ return {
209
+ reason: `The model provider blocked this request.${detail || message ? ` ${detail ?? message}` : ""}`,
210
+ safetyRefusal: true,
211
+ };
212
+ }
213
+ return {
214
+ reason:
215
+ detail && detail !== message
216
+ ? [humanizeFailureReason(message), humanizeFailureReason(detail)].filter(Boolean).join(" ")
217
+ : humanizeFailureReason(message),
218
+ safetyRefusal: false,
219
+ };
220
+ }
package/src/session-ui.ts CHANGED
@@ -25,6 +25,8 @@ export type {
25
25
  TurnSummaryOptions,
26
26
  } from "./timeline/turn-summary";
27
27
  export { QueueSurface } from "./components/queue-surface";
28
+ export { SessionConversation } from "./components/session-conversation";
29
+ export type { SessionConversationProps } from "./components/session-conversation";
28
30
  export type { QueueSurfaceProps } from "./components/queue-surface";
29
31
  export {
30
32
  SessionChrome,
@@ -38,3 +40,4 @@ export type {
38
40
  SessionChromeSignalId,
39
41
  SessionChromeSignalTone,
40
42
  } from "./components/session-chrome";
43
+ export { SessionCommandsPanel } from "./components/session-commands-panel";
@@ -8,6 +8,7 @@ const FLEET_ACTUAL_REASONS = [
8
8
  "rotation",
9
9
  "active",
10
10
  "all_capped",
11
+ "allocator_disabled",
11
12
  "none",
12
13
  ] as const;
13
14
  const FLEET_SHADOW_OUTCOMES = ["selected", "paced", "none"] as const;
@@ -334,7 +335,7 @@ function fleetDecisionSemanticsAreConsistent(input: {
334
335
  input.actualOutcome === "selected"
335
336
  ? ["lease_reused", "pin", "rotation", "active"].includes(input.actualReason)
336
337
  : input.actualOutcome === "waiting"
337
- ? input.actualReason === "all_capped"
338
+ ? ["all_capped", "allocator_disabled"].includes(input.actualReason)
338
339
  : input.actualReason === "none";
339
340
  const shadowConsistent =
340
341
  input.shadowOutcome === "selected"
@@ -28,6 +28,7 @@ const FLEET_ACTUAL_REASON_LABEL: Record<FleetDecisionItem["actualReason"], strin
28
28
  rotation: "Rotated for capacity",
29
29
  active: "Used the active subscription",
30
30
  all_capped: "All observed subscriptions were capped",
31
+ allocator_disabled: "The policy-selected subscription was disabled for new allocations",
31
32
  none: "No production candidate was selected",
32
33
  };
33
34
 
@@ -10,7 +10,7 @@ import {
10
10
  import fleetDecisionItem from "./fleet-decision-projection";
11
11
  import {
12
12
  CREDIT_EXHAUSTION_MESSAGE,
13
- humanizeFailureReason,
13
+ presentFailure,
14
14
  isCreditExhaustion,
15
15
  tryParseJson,
16
16
  } from "../lib/format";
@@ -58,6 +58,8 @@ const WORKER_SPAWN_TOOL = "session_create";
58
58
  const WORKER_MESSAGE_TOOL = "session_send_message";
59
59
  const WORKER_FAILURE_CODE_MAX_LENGTH = 128;
60
60
  const WORKER_FAILURE_MESSAGE_MAX_UTF8_BYTES = 1_024;
61
+ type PendingWaitOutcome = { id: string; reason: string; occurredAt: string };
62
+ type TrackedAgentResponse = { item: AgentMessageItem; completed: boolean };
61
63
 
62
64
  /**
63
65
  * Tools whose durable side-effect events already own the timeline (MemoryRow).
@@ -131,6 +133,8 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
131
133
  const items: TimelineItem[] = [];
132
134
  const prescan = prescanTurnAnchors(events);
133
135
  const ordered = orderTimelineEvents(events, prescan);
136
+ const pendingWaitOutcomeByTurn = new Map<string | null, PendingWaitOutcome>();
137
+ const latestAgentResponseByTurn = new Map<string | null, TrackedAgentResponse>();
134
138
  const humanInputRequests = humanInputRequestsById(events);
135
139
  const humanInputToolCallIds = new Set(
136
140
  [...humanInputRequests.values()]
@@ -220,6 +224,31 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
220
224
 
221
225
  const last = (): TimelineItem | undefined => items[items.length - 1];
222
226
 
227
+ const rememberAgentResponse = (
228
+ turnId: string | null,
229
+ item: AgentMessageItem,
230
+ completed: boolean,
231
+ ): void => {
232
+ latestAgentResponseByTurn.set(turnId, { item, completed });
233
+ };
234
+
235
+ const takePendingWaitOutcome = (turnId: string | null): PendingWaitOutcome | undefined => {
236
+ const outcome = pendingWaitOutcomeByTurn.get(turnId);
237
+ pendingWaitOutcomeByTurn.delete(turnId);
238
+ return outcome;
239
+ };
240
+
241
+ const takeAgentResponse = (turnId: string | null): TrackedAgentResponse | undefined => {
242
+ const response = latestAgentResponseByTurn.get(turnId);
243
+ latestAgentResponseByTurn.delete(turnId);
244
+ return response;
245
+ };
246
+
247
+ const clearUnscopedTerminalTracking = (): void => {
248
+ latestAgentResponseByTurn.delete(null);
249
+ pendingWaitOutcomeByTurn.delete(null);
250
+ };
251
+
223
252
  /** A new item of a different kind ends whatever was streaming at the tail. */
224
253
  const closeStreamingTail = (): void => {
225
254
  const open = last();
@@ -298,6 +327,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
298
327
  case "user.message": {
299
328
  // A steering message must not mark in-flight tools complete; it only
300
329
  // ends whatever text was streaming. Turn lifecycle events finalize.
330
+ clearUnscopedTerminalTracking();
301
331
  closeStreamingTail();
302
332
  const childCompletion = workerCompletionPayload(payload.childCompletion);
303
333
  if (childCompletion) {
@@ -381,17 +411,20 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
381
411
  const open = last();
382
412
  if (open?.kind === "agent-message" && open.streaming && open.turnId === turnId) {
383
413
  open.text += text;
414
+ rememberAgentResponse(turnId, open, false);
384
415
  break;
385
416
  }
386
417
  closeStreamingTail();
387
- items.push({
418
+ const item: AgentMessageItem = {
388
419
  kind: "agent-message",
389
420
  id: event.id,
390
421
  turnId,
391
422
  text,
392
423
  streaming: true,
393
424
  occurredAt: event.occurredAt,
394
- });
425
+ };
426
+ items.push(item);
427
+ rememberAgentResponse(turnId, item, false);
395
428
  break;
396
429
  }
397
430
 
@@ -447,10 +480,11 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
447
480
  items.splice(openIndex, 1);
448
481
  items.push(open);
449
482
  }
483
+ rememberAgentResponse(turnId, open, true);
450
484
  break;
451
485
  }
452
486
  if (text) {
453
- items.push({
487
+ const item: AgentMessageItem = {
454
488
  kind: "agent-message",
455
489
  id: event.id,
456
490
  turnId,
@@ -466,7 +500,9 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
466
500
  turnId,
467
501
  text,
468
502
  },
469
- });
503
+ };
504
+ items.push(item);
505
+ rememberAgentResponse(turnId, item, true);
470
506
  }
471
507
  break;
472
508
  }
@@ -616,6 +652,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
616
652
  }
617
653
 
618
654
  case "turn.started": {
655
+ if (turnId) clearUnscopedTerminalTracking();
619
656
  if (!turnId) break;
620
657
  const queuedAt = queuedAtByTurn.get(turnId);
621
658
  if (!queuedAt) break;
@@ -781,6 +818,20 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
781
818
  break;
782
819
  }
783
820
 
821
+ case "codex.capacity.waiting": {
822
+ items.push({
823
+ kind: "notice",
824
+ id: event.id,
825
+ tone: "waiting",
826
+ text:
827
+ stringValue(payload.detail) ??
828
+ stringValue(payload.error) ??
829
+ "Waiting for Codex capacity.",
830
+ occurredAt: event.occurredAt,
831
+ });
832
+ break;
833
+ }
834
+
784
835
  case "session.requiresAction": {
785
836
  finalizeOpen(turnId, "complete", event.occurredAt);
786
837
  items.push({
@@ -922,6 +973,8 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
922
973
  // extra agent response.
923
974
  if (payload.maintenance === "context_compaction") {
924
975
  finalizeOpen(turnId, "complete", event.occurredAt);
976
+ takeAgentResponse(turnId);
977
+ takePendingWaitOutcome(turnId);
925
978
  break;
926
979
  }
927
980
  // Credit exhaustion arrives as a NOMINALLY completed turn (`detail:
@@ -932,6 +985,8 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
932
985
  // projects exactly like a failed turn plus an explicit notice.
933
986
  if (isCreditExhaustionPayload(payload)) {
934
987
  finalizeOpen(turnId, "complete", event.occurredAt);
988
+ takeAgentResponse(turnId);
989
+ takePendingWaitOutcome(turnId);
935
990
  items.push(turnEndItem(event, "failed", CREDIT_EXHAUSTION_MESSAGE));
936
991
  items.push({
937
992
  kind: "notice",
@@ -942,12 +997,69 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
942
997
  });
943
998
  break;
944
999
  }
1000
+ const latestAgentResponse = takeAgentResponse(turnId);
1001
+ const finalOutput = stringValue(payload.output);
1002
+ const visibleFinalOutput = stripOpaqueCitationTokens(finalOutput).trim();
1003
+ const visibleTrackedResponse = stripOpaqueCitationTokens(
1004
+ latestAgentResponse?.item.text ?? "",
1005
+ ).trim();
1006
+ const finalOutputMirrorsCommentary =
1007
+ latestAgentResponse?.item.phase === "commentary" &&
1008
+ visibleTrackedResponse === visibleFinalOutput;
1009
+ const hasAuthoritativeFinalOutput =
1010
+ Boolean(visibleFinalOutput) && !finalOutputMirrorsCommentary;
1011
+ const pendingWaitOutcome = takePendingWaitOutcome(turnId);
1012
+ if (hasAuthoritativeFinalOutput) {
1013
+ // `agent.message.completed` and `turn.completed` normally commit
1014
+ // together, but legacy or partially compacted ledgers may retain only
1015
+ // the terminal output receipt. Keep that authoritative response
1016
+ // visible instead of leaving it trapped in raw audit data.
1017
+ if (
1018
+ latestAgentResponse &&
1019
+ !latestAgentResponse.completed &&
1020
+ finalOutput.startsWith(latestAgentResponse.item.text)
1021
+ ) {
1022
+ latestAgentResponse.item.text = finalOutput;
1023
+ latestAgentResponse.item.streaming = false;
1024
+ latestAgentResponse.item.occurredAt = event.occurredAt;
1025
+ const responseIndex = items.indexOf(latestAgentResponse.item);
1026
+ if (responseIndex >= 0 && responseIndex < items.length - 1) {
1027
+ items.splice(responseIndex, 1);
1028
+ items.push(latestAgentResponse.item);
1029
+ }
1030
+ } else if (visibleTrackedResponse !== visibleFinalOutput) {
1031
+ items.push({
1032
+ kind: "agent-message",
1033
+ id: `${event.id}-output-message`,
1034
+ turnId,
1035
+ text: finalOutput,
1036
+ streaming: false,
1037
+ occurredAt: event.occurredAt,
1038
+ });
1039
+ }
1040
+ }
945
1041
  finalizeOpen(turnId, "complete", event.occurredAt);
946
1042
  items.push(turnEndItem(event, "complete", null));
1043
+ const hasCompletedFinalResponse =
1044
+ latestAgentResponse?.completed === true &&
1045
+ latestAgentResponse.item.phase !== "commentary" &&
1046
+ Boolean(visibleTrackedResponse);
1047
+ if (!hasAuthoritativeFinalOutput && !hasCompletedFinalResponse && pendingWaitOutcome) {
1048
+ items.push({
1049
+ kind: "notice",
1050
+ id: `${pendingWaitOutcome.id}-visible-outcome`,
1051
+ tone: "waiting",
1052
+ text: waitingOutcomeText(pendingWaitOutcome.reason),
1053
+ recordedOutcome: true,
1054
+ occurredAt: pendingWaitOutcome.occurredAt,
1055
+ });
1056
+ }
947
1057
  break;
948
1058
  }
949
1059
 
950
1060
  case "turn.failed": {
1061
+ takeAgentResponse(turnId);
1062
+ takePendingWaitOutcome(turnId);
951
1063
  const hadActivity = hasTurnActivity(items, turnId);
952
1064
  // Credit death can hide behind fields `failureMessage` doesn't read
953
1065
  // (detail/segmentLimit), so classify the whole payload before falling
@@ -980,6 +1092,8 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
980
1092
  if (turnId && !prescan.startedTurnIds.has(turnId)) {
981
1093
  break;
982
1094
  }
1095
+ takeAgentResponse(turnId);
1096
+ takePendingWaitOutcome(turnId);
983
1097
  const hadActivity = hasTurnActivity(items, turnId);
984
1098
  finalizeOpen(turnId, "cancelled", event.occurredAt);
985
1099
  items.push(turnEndItem(event, "cancelled", null));
@@ -1025,6 +1139,9 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
1025
1139
  case "goal.cleared":
1026
1140
  case "goal.held":
1027
1141
  case "goal.continuation": {
1142
+ if (event.type === "goal.held" && payload.actor === "agent") {
1143
+ rememberPendingWaitOutcome(pendingWaitOutcomeByTurn, event, payload, turnId);
1144
+ }
1028
1145
  // Agent tool mutations already appear as tool-call rows in the activity
1029
1146
  // cluster. Re-emitting them as GoalRow landmarks splits "N steps" mid-turn.
1030
1147
  if (shouldSuppressAgentGoalLandmark(event.type, payload)) {
@@ -1043,6 +1160,13 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
1043
1160
  break;
1044
1161
  }
1045
1162
 
1163
+ case "session.wait.started": {
1164
+ if (payload.actor === "agent") {
1165
+ rememberPendingWaitOutcome(pendingWaitOutcomeByTurn, event, payload, turnId);
1166
+ }
1167
+ break;
1168
+ }
1169
+
1046
1170
  default:
1047
1171
  break;
1048
1172
  }
@@ -1730,8 +1854,7 @@ function isTurnBoundary(group: TimelineGroup | undefined): boolean {
1730
1854
  (group.item.kind === "user-message" ||
1731
1855
  group.item.kind === "human-input" ||
1732
1856
  group.item.kind === "context-compaction" ||
1733
- (group.item.kind === "machine-input-batch" &&
1734
- group.item.members.some((member) => member.kind === "media_generation_result")) ||
1857
+ group.item.kind === "machine-input-batch" ||
1735
1858
  (group.item.kind === "notice" && group.item.tone === "input")))
1736
1859
  );
1737
1860
  }
@@ -1898,6 +2021,8 @@ function machineInputMembers(value: unknown): MachineInputBatchItem["members"] {
1898
2021
  const kinds = new Set<MachineInputBatchItem["members"][number]["kind"]>([
1899
2022
  "scheduled_occurrence",
1900
2023
  "goal_continuation",
2024
+ "background_command_result",
2025
+ "session_wait_timeout",
1901
2026
  "agent_message",
1902
2027
  "agent_steer_instruction",
1903
2028
  "child_terminal_result",
@@ -1953,6 +2078,8 @@ const SESSION_STATUSES: readonly SessionStatus[] = [
1953
2078
  "running",
1954
2079
  "idle",
1955
2080
  "requires_action",
2081
+ "recovering",
2082
+ "waiting_capacity",
1956
2083
  "failed",
1957
2084
  "cancelled",
1958
2085
  ];
@@ -2148,15 +2275,7 @@ function elapsedDurationMs(startedAt: string, completedAt: string): number | nul
2148
2275
  }
2149
2276
 
2150
2277
  function failureMessage(payload: Record<string, unknown>): string | null {
2151
- for (const key of ["error", "message"] as const) {
2152
- const value = payload[key];
2153
- if (typeof value === "string" && value.trim().length > 0) {
2154
- // Auth/quota provider errors are rewritten for the right audience
2155
- // (raw text remains in the event payload for debug surfaces).
2156
- return humanizeFailureReason(value);
2157
- }
2158
- }
2159
- return null;
2278
+ return presentFailure(payload).reason;
2160
2279
  }
2161
2280
 
2162
2281
  function goalText(payload: Record<string, unknown>): string | null {
@@ -2173,11 +2292,30 @@ function goalText(payload: Record<string, unknown>): string | null {
2173
2292
  return null;
2174
2293
  }
2175
2294
 
2295
+ function rememberPendingWaitOutcome(
2296
+ pending: Map<string | null, PendingWaitOutcome>,
2297
+ event: SessionEvent,
2298
+ payload: Record<string, unknown>,
2299
+ eventTurnId: string | null,
2300
+ ): void {
2301
+ const reason = stringValue(payload.reason).trim();
2302
+ if (!reason) return;
2303
+ const turnId =
2304
+ eventTurnId || stringValue(payload.waitTurnId) || stringValue(payload.turnId) || null;
2305
+ pending.set(turnId, { id: event.id, reason, occurredAt: event.occurredAt });
2306
+ }
2307
+
2308
+ function waitingOutcomeText(reason: string): string {
2309
+ return /^waiting\b/i.test(reason) ? reason : `Waiting: ${reason}`;
2310
+ }
2311
+
2176
2312
  /**
2177
2313
  * Agent-owned goal mutations already have an in-cluster tool row. Suppress the
2178
- * breakaway landmark for those only. `goal.completed` has no actor field today
2179
- * and is only emitted by the agent tool, so it is always suppressed. API /
2180
- * system / create-session / continuation landmarks stay visible.
2314
+ * breakaway landmark for those only. Empty wait turns retain the durable hold
2315
+ * reason and surface it after the folded steps. `goal.completed` has no actor
2316
+ * field today and is only emitted by the agent tool, so it is always
2317
+ * suppressed. API / system / create-session / continuation landmarks stay
2318
+ * visible.
2181
2319
  */
2182
2320
  function shouldSuppressAgentGoalLandmark(type: string, payload: Record<string, unknown>): boolean {
2183
2321
  if (type === "goal.completed") {
@@ -13,7 +13,7 @@ import type { ToolCallItem } from "./types";
13
13
  Resolution order (most → least specific):
14
14
  1. exact match on `raw.type` (e.g. "apply_patch_call", "computer_call")
15
15
  2. exact match on the tool `name` (e.g. "exec_command", "web_search_call")
16
- 3. leaf match after MCP `__` prefix (e.g. opengeni__environment_set_variable)
16
+ 3. allowed leaf match after MCP `__` prefix (e.g. opengeni__environment_set_variable)
17
17
  4. the registry's generic fallback
18
18
 
19
19
  A consumer extends the defaults without forking by passing overrides to
@@ -48,7 +48,13 @@ export type ToolRenderer = ComponentType<ToolRendererProps>;
48
48
  /** A registry entry: which key it matches and the component that renders it. */
49
49
  export type ToolRegistryEntry =
50
50
  | { match: "rawType"; type: string; render: ToolRenderer }
51
- | { match: "name"; name: string; render: ToolRenderer };
51
+ | {
52
+ match: "name";
53
+ name: string;
54
+ render: ToolRenderer;
55
+ /** Disable untrusted `<server>__${name}` leaf matching for identity-sensitive renderers. */
56
+ matchPrefixedLeaf?: boolean | undefined;
57
+ };
52
58
 
53
59
  export type ToolRegistry = {
54
60
  /** Resolve the renderer for a call (never null — falls back to generic). */
@@ -92,6 +98,7 @@ export function createToolRegistry(
92
98
 
93
99
  const byRawType = new Map<string, ToolRenderer>();
94
100
  const byName = new Map<string, ToolRenderer>();
101
+ const byPrefixedLeaf = new Map<string, ToolRenderer>();
95
102
  for (const entry of entries) {
96
103
  if (entry.match === "rawType") {
97
104
  if (!byRawType.has(entry.type)) {
@@ -100,6 +107,11 @@ export function createToolRegistry(
100
107
  } else if (!byName.has(entry.name)) {
101
108
  byName.set(entry.name, entry.render);
102
109
  }
110
+ if (entry.match === "name" && entry.matchPrefixedLeaf !== false) {
111
+ if (!byPrefixedLeaf.has(entry.name)) {
112
+ byPrefixedLeaf.set(entry.name, entry.render);
113
+ }
114
+ }
103
115
  }
104
116
 
105
117
  const resolve = (item: ToolCallItem): ToolRenderer => {
@@ -116,7 +128,7 @@ export function createToolRegistry(
116
128
  }
117
129
  const leaf = mcpToolLeaf(item.name);
118
130
  if (leaf !== item.name) {
119
- const byLeaf = byName.get(leaf);
131
+ const byLeaf = byPrefixedLeaf.get(leaf);
120
132
  if (byLeaf) {
121
133
  return byLeaf;
122
134
  }
@@ -1085,6 +1085,98 @@ function SandboxFilePublishRenderer({ item, loadRetainedArtifact }: ToolRenderer
1085
1085
  );
1086
1086
  }
1087
1087
 
1088
+ type PublishedSiteReceipt = {
1089
+ workspaceId: string;
1090
+ artifactId: string;
1091
+ title: string;
1092
+ revision: number;
1093
+ replayed: boolean;
1094
+ };
1095
+
1096
+ function publishedSiteReceipt(output: unknown): PublishedSiteReceipt | null {
1097
+ const { text, isError } = unwrapMcpOutput(output);
1098
+ if (isError) return null;
1099
+ const parsed = tryParseJson(text);
1100
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
1101
+ const artifact = (parsed as Record<string, unknown>).artifact;
1102
+ const version = (parsed as Record<string, unknown>).version;
1103
+ if (
1104
+ !artifact ||
1105
+ typeof artifact !== "object" ||
1106
+ Array.isArray(artifact) ||
1107
+ !version ||
1108
+ typeof version !== "object" ||
1109
+ Array.isArray(version)
1110
+ ) {
1111
+ return null;
1112
+ }
1113
+ const artifactRecord = artifact as Record<string, unknown>;
1114
+ const versionRecord = version as Record<string, unknown>;
1115
+ if (
1116
+ typeof artifactRecord.workspaceId !== "string" ||
1117
+ typeof artifactRecord.id !== "string" ||
1118
+ typeof artifactRecord.title !== "string" ||
1119
+ typeof versionRecord.revision !== "number" ||
1120
+ !Number.isInteger(versionRecord.revision) ||
1121
+ versionRecord.revision < 1
1122
+ ) {
1123
+ return null;
1124
+ }
1125
+ return {
1126
+ workspaceId: artifactRecord.workspaceId,
1127
+ artifactId: artifactRecord.id,
1128
+ title: artifactRecord.title,
1129
+ revision: versionRecord.revision,
1130
+ replayed: (parsed as Record<string, unknown>).replayed === true,
1131
+ };
1132
+ }
1133
+
1134
+ function SiteOpenLink({ receipt }: { receipt: PublishedSiteReceipt }) {
1135
+ const href = `/workspaces/${encodeURIComponent(receipt.workspaceId)}/artifacts/${encodeURIComponent(receipt.artifactId)}`;
1136
+ return (
1137
+ <a
1138
+ href={href}
1139
+ aria-label={`Open ${receipt.title}`}
1140
+ className="inline-flex min-h-7 items-center rounded-og-sm px-2 text-og-sm font-medium text-og-accent-strong hover:bg-og-surface-2 hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent pointer-coarse:min-h-10"
1141
+ onClick={(event) => event.stopPropagation()}
1142
+ onKeyDown={(event) => event.stopPropagation()}
1143
+ >
1144
+ Open
1145
+ </a>
1146
+ );
1147
+ }
1148
+
1149
+ function SiteArtifactRenderer({ item }: ToolRendererProps) {
1150
+ const leaf = mcpToolLeaf(item.name);
1151
+ const publishingExisting = leaf === "artifacts_publish";
1152
+ if (item.status === "running") {
1153
+ return (
1154
+ <ActivityDisclosure
1155
+ icon={<PanelsTopLeftIcon className={ICON_SIZE} />}
1156
+ iconTone="running"
1157
+ title={publishingExisting ? "Publishing Site update" : "Publishing Site"}
1158
+ running
1159
+ preview={<RunningPreview>retaining source and compiled HTML…</RunningPreview>}
1160
+ />
1161
+ );
1162
+ }
1163
+ const receipt = publishedSiteReceipt(item.output);
1164
+ if (!receipt || item.status === "failed") return <GenericRenderer item={item} />;
1165
+ return (
1166
+ <ActivityDisclosure
1167
+ icon={<PanelsTopLeftIcon className={ICON_SIZE} />}
1168
+ iconTone="accent"
1169
+ title={publishingExisting ? `Updated ${receipt.title}` : `Published ${receipt.title}`}
1170
+ media={<SiteOpenLink receipt={receipt} />}
1171
+ >
1172
+ <BodyNote>
1173
+ Version {receipt.revision} is live
1174
+ {receipt.replayed ? " (replayed from the original publication)." : "."}
1175
+ </BodyNote>
1176
+ </ActivityDisclosure>
1177
+ );
1178
+ }
1179
+
1088
1180
  function GeneratedImageDisclosure({
1089
1181
  receipt,
1090
1182
  load,
@@ -2198,7 +2290,7 @@ function goalToolPreview(name: string, args: unknown): string | null {
2198
2290
  leaf !== "goal_update" &&
2199
2291
  leaf !== "goal_complete" &&
2200
2292
  leaf !== "goal_pause" &&
2201
- leaf !== "goal_wait"
2293
+ leaf !== "wait_for_input"
2202
2294
  ) {
2203
2295
  return null;
2204
2296
  }
@@ -2308,6 +2400,30 @@ const BASE_ENTRIES: ToolRegistryEntry[] = [
2308
2400
  { match: "name", name: "tool_search", render: ToolSearchRenderer },
2309
2401
  { match: "name", name: "view_image", render: ViewImageRenderer },
2310
2402
  { match: "name", name: "sandbox_file_publish", render: SandboxFilePublishRenderer },
2403
+ {
2404
+ match: "name",
2405
+ name: "artifacts_create",
2406
+ render: SiteArtifactRenderer,
2407
+ matchPrefixedLeaf: false,
2408
+ },
2409
+ {
2410
+ match: "name",
2411
+ name: "artifacts_publish",
2412
+ render: SiteArtifactRenderer,
2413
+ matchPrefixedLeaf: false,
2414
+ },
2415
+ {
2416
+ match: "name",
2417
+ name: "opengeni__artifacts_create",
2418
+ render: SiteArtifactRenderer,
2419
+ matchPrefixedLeaf: false,
2420
+ },
2421
+ {
2422
+ match: "name",
2423
+ name: "opengeni__artifacts_publish",
2424
+ render: SiteArtifactRenderer,
2425
+ matchPrefixedLeaf: false,
2426
+ },
2311
2427
  { match: "name", name: "environment_set_variable", render: SecretSetRenderer },
2312
2428
  { match: "name", name: "variable_set_set_variable", render: SecretSetRenderer },
2313
2429
  { match: "name", name: "search_documents", render: DocsSearchRenderer },
@@ -279,7 +279,14 @@ export type FleetDecisionItem = {
279
279
  policyVersion: "adaptive-shadow-v1";
280
280
  actualOutcome: "selected" | "waiting" | "none";
281
281
  actualCandidateKey: string | null;
282
- actualReason: "lease_reused" | "pin" | "rotation" | "active" | "all_capped" | "none";
282
+ actualReason:
283
+ | "lease_reused"
284
+ | "pin"
285
+ | "rotation"
286
+ | "active"
287
+ | "all_capped"
288
+ | "allocator_disabled"
289
+ | "none";
283
290
  shadowOutcome: "selected" | "paced" | "none";
284
291
  shadowCandidateKey: string | null;
285
292
  shadowReason:
@@ -342,6 +349,8 @@ export type NoticeItem = {
342
349
  id: string;
343
350
  tone: "waiting" | "cancelled" | "failed" | "input";
344
351
  text: string;
352
+ /** A preserved turn-end outcome, not a claim about current session state. */
353
+ recordedOutcome?: true;
345
354
  /** Optional evidence kept inspectable without overwhelming the main rail. */
346
355
  details?: { label: string; value: unknown };
347
356
  action?: { label: string; url: string };
@@ -371,6 +380,8 @@ export type MachineInputMember = {
371
380
  kind:
372
381
  | "scheduled_occurrence"
373
382
  | "goal_continuation"
383
+ | "background_command_result"
384
+ | "session_wait_timeout"
374
385
  | "agent_message"
375
386
  | "agent_steer_instruction"
376
387
  | "child_terminal_result"