@opengeni/react 3.8.0-canary.1 → 4.0.1-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 (63) hide show
  1. package/README.md +10 -24
  2. package/dist/artifacts.js +7 -7
  3. package/dist/{browser-viewer-SS46LUAC.js → browser-viewer-NTD6UIPR.js} +3 -3
  4. package/dist/{chunk-VEPNNDNG.js → chunk-24M4YBCL.js} +4 -4
  5. package/dist/{chunk-ELIYWBZR.js → chunk-BV5A4OTE.js} +4 -4
  6. package/dist/{chunk-7W6PVVTK.js → chunk-CQG7YUJB.js} +2582 -1037
  7. package/dist/chunk-CQG7YUJB.js.map +1 -0
  8. package/dist/{chunk-BXJOQFCD.js → chunk-HUMIRRYI.js} +84 -221
  9. package/dist/chunk-HUMIRRYI.js.map +1 -0
  10. package/dist/{chunk-7BWFU6H4.js → chunk-Z7WBDB4E.js} +395 -287
  11. package/dist/chunk-Z7WBDB4E.js.map +1 -0
  12. package/dist/components/human-input-form.d.ts +3 -1
  13. package/dist/components/human-input-surface.d.ts +3 -2
  14. package/dist/components/markdown-table-layout.d.ts +9 -0
  15. package/dist/components/session-conversation.d.ts +2 -0
  16. package/dist/composer.js +2 -3
  17. package/dist/{computer-viewer-FWEZISW4.js → computer-viewer-7TEZC6Y2.js} +3 -3
  18. package/dist/hooks/use-codex-accounts.d.ts +11 -1
  19. package/dist/hooks/use-session-events.d.ts +2 -2
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +44 -34
  22. package/dist/index.js.map +1 -1
  23. package/dist/interaction.js +3 -3
  24. package/dist/markdown-table-layout-3Z3AWBY7.js +71 -0
  25. package/dist/markdown-table-layout-3Z3AWBY7.js.map +1 -0
  26. package/dist/older-history.d.ts +3 -2
  27. package/dist/{platform-activity-row-BJHUHXAT.js → platform-activity-row-27LKAWZD.js} +2 -2
  28. package/dist/platform-activity-row-27LKAWZD.js.map +1 -0
  29. package/dist/realtime.js +3 -3
  30. package/dist/session-ui.js +6 -9
  31. package/dist/session.js +1 -1
  32. package/dist/timeline/types.d.ts +3 -0
  33. package/package.json +2 -6
  34. package/src/components/human-input-form.tsx +185 -53
  35. package/src/components/human-input-surface.tsx +3 -0
  36. package/src/components/markdown-table-layout.ts +85 -0
  37. package/src/components/markdown.tsx +20 -2
  38. package/src/components/message-timeline.tsx +47 -7
  39. package/src/components/session-commands-panel.tsx +7 -1
  40. package/src/components/session-conversation.tsx +4 -1
  41. package/src/hooks/use-codex-accounts.ts +32 -5
  42. package/src/hooks/use-session-events.ts +16 -277
  43. package/src/index.ts +0 -1
  44. package/src/older-history.ts +22 -4
  45. package/src/timeline/platform-activity-row.tsx +4 -1
  46. package/src/timeline/projection.ts +99 -9
  47. package/src/timeline/types.ts +3 -0
  48. package/dist/chat.d.ts +0 -37
  49. package/dist/chat.js +0 -315
  50. package/dist/chat.js.map +0 -1
  51. package/dist/chunk-7BWFU6H4.js.map +0 -1
  52. package/dist/chunk-7W6PVVTK.js.map +0 -1
  53. package/dist/chunk-BXJOQFCD.js.map +0 -1
  54. package/dist/chunk-HQV2I5HV.js +0 -124
  55. package/dist/chunk-HQV2I5HV.js.map +0 -1
  56. package/dist/chunk-YAIPFO5D.js +0 -1416
  57. package/dist/chunk-YAIPFO5D.js.map +0 -1
  58. package/dist/platform-activity-row-BJHUHXAT.js.map +0 -1
  59. package/src/chat.tsx +0 -393
  60. /package/dist/{browser-viewer-SS46LUAC.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
  61. /package/dist/{chunk-VEPNNDNG.js.map → chunk-24M4YBCL.js.map} +0 -0
  62. /package/dist/{chunk-ELIYWBZR.js.map → chunk-BV5A4OTE.js.map} +0 -0
  63. /package/dist/{computer-viewer-FWEZISW4.js.map → computer-viewer-7TEZC6Y2.js.map} +0 -0
@@ -102,34 +102,8 @@ const FOREGROUND_COMPACT_CATCHUP_MAX_GROUPS = 16;
102
102
  const FOREGROUND_COMPACT_CATCHUP_MAX_BYTES = 512 * 1024;
103
103
  const EMPTY_EVENTS: SessionEvent[] = [];
104
104
  const encoder = new TextEncoder();
105
- const decoder = new TextDecoder();
106
-
107
- const BROWSER_EVENT_TYPE_MAX_BYTES = 256;
108
- const BROWSER_EVENT_ID_MAX_BYTES = 256;
109
- const BROWSER_EVENT_CLIENT_ID_MAX_BYTES = 4 * 1024;
110
- const BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES = 4 * 1024;
111
- const BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES = 48 * 1024;
112
- const BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS = [
113
- "id",
114
- "callId",
115
- "call_id",
116
- "name",
117
- "toolName",
118
- "status",
119
- "code",
120
- "isError",
121
- "stream",
122
- "commandId",
123
- "sequence",
124
- "coalescedUntil",
125
- "coalescedCount",
126
- "firstSequence",
127
- "lastSequence",
128
- ] as const;
129
-
130
105
  export const SESSION_EVENT_BROWSER_MAX_BYTES = 8 * 1024 * 1024;
131
106
  export const SESSION_EVENT_BROWSER_MAX_COUNT = 10_000;
132
- export const SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES = 96 * 1024;
133
107
  export const SESSION_EVENT_BROWSER_PENDING_MAX_BYTES = 1024 * 1024;
134
108
  export const SESSION_EVENT_BROWSER_PENDING_MAX_COUNT = 256;
135
109
 
@@ -482,7 +456,7 @@ export function useSessionEvents(
482
456
  });
483
457
  for await (const event of stream) {
484
458
  if (!isCurrent()) break;
485
- const boundedEvent = boundBrowserLegacyEvent(event);
459
+ const boundedEvent = event;
486
460
  const boundedEventBytes = browserJsonBytes(boundedEvent);
487
461
  const separatorBytes = pending.length === 0 ? 0 : 1;
488
462
  if (
@@ -556,7 +530,7 @@ export function useSessionEvents(
556
530
 
557
531
  const loadOlder = useCallback(
558
532
  (): OlderHistoryLoadReceipt =>
559
- createOlderHistoryLoadReceipt(async (markCommitted) => {
533
+ createOlderHistoryLoadReceipt(async (markCommitted, preserveTail, markTailPreserved) => {
560
534
  if (!sessionId || navigationBusy() || !hasOlderRef.current) {
561
535
  return false;
562
536
  }
@@ -592,11 +566,17 @@ export function useSessionEvents(
592
566
  // Freeze the live iterator before replacing its in-memory window. Rows
593
567
  // pending in the aborted iterator were never cursor-committed and will
594
568
  // be replayed from the retained high-water mark below.
595
- streamAbortRef.current?.abort();
596
- const status = observeSessionStatus(window.events, sessionStatusRef);
597
569
  const next = boundBrowserSessionEventWindow([...window.events, ...current.events], {
598
570
  direction: "oldest",
599
571
  });
572
+ if (preserveTail && maxResumeSequence(next.events) < maxResumeSequence(current.events)) {
573
+ // Automatic viewport filling must not navigate away from the
574
+ // reader's retained tail. Explicit history navigation may do so.
575
+ markTailPreserved();
576
+ return false;
577
+ }
578
+ streamAbortRef.current?.abort();
579
+ const status = observeSessionStatus(window.events, sessionStatusRef);
600
580
  const retained = {
601
581
  ...next,
602
582
  truncated: current.truncated || next.truncated,
@@ -904,7 +884,8 @@ export function useSessionEvents(
904
884
  * deliberately separate from durable history and transport paging: when a
905
885
  * backward page evicts the live tail, the hook enters bounded history mode and
906
886
  * preserves the highest-ever-observed sequence separately for forward paging.
907
- * The source event remains durable in PostgreSQL throughout.
887
+ * The source event remains durable in PostgreSQL throughout. An event larger
888
+ * than the byte target is retained alone, never replaced by a lossy preview.
908
889
  */
909
890
  export function boundBrowserSessionEventWindow(
910
891
  events: readonly SessionEvent[],
@@ -916,7 +897,7 @@ export function boundBrowserSessionEventWindow(
916
897
  ): BrowserSessionEventWindow {
917
898
  const maxBytes = Math.max(1024, options.maxBytes ?? SESSION_EVENT_BROWSER_MAX_BYTES);
918
899
  const maxCount = Math.max(1, Math.floor(options.maxCount ?? SESSION_EVENT_BROWSER_MAX_COUNT));
919
- const safe = events.map(boundBrowserLegacyEvent);
900
+ const safe = events;
920
901
  const selected: SessionEvent[] = [];
921
902
  let bytes = 2; // []
922
903
  const direction = options.direction ?? "newest";
@@ -927,7 +908,7 @@ export function boundBrowserSessionEventWindow(
927
908
  const event = safe[index]!;
928
909
  const eventBytes = browserJsonBytes(event);
929
910
  const separator = selected.length === 0 ? 0 : 1;
930
- if (bytes + separator + eventBytes > maxBytes) break;
911
+ if (selected.length > 0 && bytes + separator + eventBytes > maxBytes) break;
931
912
  selected.push(event);
932
913
  bytes += separator + eventBytes;
933
914
  }
@@ -939,248 +920,8 @@ export function boundBrowserSessionEventWindow(
939
920
  };
940
921
  }
941
922
 
942
- function boundBrowserLegacyEvent(event: SessionEvent): SessionEvent {
943
- // The server-side canonical projection lives in @opengeni/contracts. The
944
- // publishable React package may depend only on the zero-runtime-dependency
945
- // SDK, so this is intentionally a last-resort client guard rather than a
946
- // second durable representation. Reconstructing the SDK wire shape prevents
947
- // legacy/malformed extra properties from bypassing the browser byte cap.
948
- const serialized = browserSerialize(event);
949
- const originalBytes = serialized.serializable
950
- ? encoder.encode(serialized.value).byteLength
951
- : null;
952
- const typeIsSafe =
953
- browserUtf8Bytes(event.type) <= BROWSER_EVENT_TYPE_MAX_BYTES &&
954
- !event.type.includes("\n") &&
955
- !event.type.includes("\r");
956
- const clientEventId = boundBrowserOptionalText(
957
- event.clientEventId,
958
- BROWSER_EVENT_CLIENT_ID_MAX_BYTES,
959
- );
960
- const duplicateReason = boundBrowserOptionalText(
961
- event.duplicateReason,
962
- BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES,
963
- );
964
-
965
- if (
966
- serialized.serializable &&
967
- originalBytes !== null &&
968
- originalBytes <= SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES &&
969
- typeIsSafe &&
970
- clientEventId === event.clientEventId &&
971
- duplicateReason === event.duplicateReason
972
- ) {
973
- return event;
974
- }
975
-
976
- const envelopeProjection = [
977
- !typeIsSafe
978
- ? browserEnvelopeFieldProjection("type", event.type, "session.event.envelope_omitted")
979
- : null,
980
- clientEventId !== event.clientEventId
981
- ? browserEnvelopeFieldProjection("clientEventId", event.clientEventId, clientEventId)
982
- : null,
983
- duplicateReason !== event.duplicateReason
984
- ? browserEnvelopeFieldProjection("duplicateReason", event.duplicateReason, duplicateReason)
985
- : null,
986
- ].filter((field) => field !== null);
987
- const payloadSerialization = browserSerialize(event.payload);
988
- const payloadBytes = payloadSerialization.serializable
989
- ? encoder.encode(payloadSerialization.value).byteLength
990
- : null;
991
- const preview = truncateBrowserUtf8Middle(
992
- payloadSerialization.value,
993
- BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES,
994
- );
995
- const truncation = {
996
- truncated: true as const,
997
- surface: "browser_legacy_guard" as const,
998
- reason: serialized.serializable ? "event_envelope_bytes_exceeded" : "event_not_serializable",
999
- originalBytes,
1000
- deliveredBytes: 0,
1001
- omittedBytes: originalBytes,
1002
- estimatedOriginalTokens: originalBytes === null ? null : Math.ceil(originalBytes / 4),
1003
- estimatedDeliveredTokens: 0,
1004
- fullEvidence: { available: false as const, reason: "not_retained" as const },
1005
- details: [
1006
- {
1007
- path: "$.payload",
1008
- kind: "payload_preview",
1009
- originalBytes: payloadBytes,
1010
- deliveredBytes: browserUtf8Bytes(preview),
1011
- },
1012
- ],
1013
- };
1014
- const payload: Record<string, unknown> = {
1015
- ...browserPayloadIdentity(event.payload),
1016
- preview,
1017
- ...(envelopeProjection.length > 0
1018
- ? {
1019
- originalType: boundBrowserText(event.type, BROWSER_EVENT_TYPE_MAX_BYTES),
1020
- envelopeProjection: {
1021
- truncated: true,
1022
- surface: "browser_legacy_guard",
1023
- fields: envelopeProjection,
1024
- },
1025
- }
1026
- : {}),
1027
- truncation,
1028
- };
1029
- const bounded: SessionEvent = {
1030
- id: boundBrowserText(event.id, BROWSER_EVENT_ID_MAX_BYTES),
1031
- workspaceId: boundBrowserText(event.workspaceId, BROWSER_EVENT_ID_MAX_BYTES),
1032
- sessionId: boundBrowserText(event.sessionId, BROWSER_EVENT_ID_MAX_BYTES),
1033
- sequence: event.sequence,
1034
- type: typeIsSafe ? event.type : "session.event.envelope_omitted",
1035
- payload,
1036
- occurredAt: boundBrowserText(event.occurredAt, BROWSER_EVENT_ID_MAX_BYTES),
1037
- clientEventId,
1038
- turnId: boundBrowserOptionalText(event.turnId, BROWSER_EVENT_ID_MAX_BYTES),
1039
- turnGeneration: event.turnGeneration,
1040
- turnAttemptId: boundBrowserOptionalText(event.turnAttemptId, BROWSER_EVENT_ID_MAX_BYTES),
1041
- turnAssociation: event.turnAssociation,
1042
- duplicateOfEventId: boundBrowserOptionalText(
1043
- event.duplicateOfEventId,
1044
- BROWSER_EVENT_ID_MAX_BYTES,
1045
- ),
1046
- duplicateReason,
1047
- };
1048
-
1049
- settleBrowserEventTruncation(bounded, truncation);
1050
- if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
1051
- payload.preview = truncateBrowserUtf8Middle(String(payload.preview), 4 * 1024);
1052
- truncation.details = truncation.details.slice(0, 1);
1053
- settleBrowserEventTruncation(bounded, truncation);
1054
- }
1055
- if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
1056
- bounded.clientEventId = null;
1057
- bounded.duplicateReason = null;
1058
- payload.preview = "[legacy event omitted at the browser byte boundary]";
1059
- settleBrowserEventTruncation(bounded, truncation);
1060
- }
1061
- return bounded;
1062
- }
1063
-
1064
923
  function browserJsonBytes(value: unknown): number {
1065
- return encoder.encode(browserSerialize(value).value).byteLength;
1066
- }
1067
-
1068
- function browserSerialize(value: unknown): { value: string; serializable: boolean } {
1069
- try {
1070
- const serialized = JSON.stringify(value);
1071
- return { value: serialized === undefined ? "null" : serialized, serializable: true };
1072
- } catch {
1073
- return {
1074
- value: '"[unserializable event payload omitted at browser boundary]"',
1075
- serializable: false,
1076
- };
1077
- }
1078
- }
1079
-
1080
- function browserPayloadIdentity(payload: unknown): Record<string, unknown> {
1081
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) return {};
1082
- const record = payload as Record<string, unknown>;
1083
- const identity: Record<string, unknown> = {};
1084
- for (const field of BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS) {
1085
- const value = record[field];
1086
- if (typeof value === "string") {
1087
- identity[field] = boundBrowserText(value, BROWSER_EVENT_ID_MAX_BYTES);
1088
- } else if (typeof value === "number" || typeof value === "boolean" || value === null) {
1089
- identity[field] = value;
1090
- }
1091
- }
1092
- return identity;
1093
- }
1094
-
1095
- function browserEnvelopeFieldProjection(
1096
- field: string,
1097
- original: string | null | undefined,
1098
- delivered: string | null | undefined,
1099
- ): { field: string; originalBytes: number; deliveredBytes: number } {
1100
- return {
1101
- field,
1102
- originalBytes: typeof original === "string" ? browserUtf8Bytes(original) : 0,
1103
- deliveredBytes: typeof delivered === "string" ? browserUtf8Bytes(delivered) : 0,
1104
- };
1105
- }
1106
-
1107
- function boundBrowserOptionalText<T extends string | null | undefined>(
1108
- value: T,
1109
- maxBytes: number,
1110
- ): T {
1111
- return (typeof value === "string" ? boundBrowserText(value, maxBytes) : value) as T;
1112
- }
1113
-
1114
- function boundBrowserText(value: string, maxBytes: number): string {
1115
- const bytes = encoder.encode(value);
1116
- if (bytes.byteLength <= maxBytes) return value;
1117
- const marker = "…[truncated]";
1118
- const prefixBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
1119
- let prefixEnd = Math.min(prefixBudget, bytes.byteLength);
1120
- while (
1121
- prefixEnd > 0 &&
1122
- prefixEnd < bytes.byteLength &&
1123
- isBrowserUtf8Continuation(bytes[prefixEnd]!)
1124
- ) {
1125
- prefixEnd -= 1;
1126
- }
1127
- return `${decoder.decode(bytes.subarray(0, prefixEnd))}${marker}`;
1128
- }
1129
-
1130
- function truncateBrowserUtf8Middle(value: string, maxBytes: number): string {
1131
- const bytes = encoder.encode(value);
1132
- if (bytes.byteLength <= maxBytes) return value;
1133
- const marker = `…[${bytes.byteLength - maxBytes} bytes omitted]…`;
1134
- const contentBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
1135
- const leftBudget = Math.floor(contentBudget / 2);
1136
- const rightBudget = contentBudget - leftBudget;
1137
- let leftEnd = Math.min(leftBudget, bytes.byteLength);
1138
- while (leftEnd > 0 && leftEnd < bytes.byteLength && isBrowserUtf8Continuation(bytes[leftEnd]!)) {
1139
- leftEnd -= 1;
1140
- }
1141
- let rightStart = Math.max(0, bytes.byteLength - rightBudget);
1142
- while (rightStart < bytes.byteLength && isBrowserUtf8Continuation(bytes[rightStart]!)) {
1143
- rightStart += 1;
1144
- }
1145
- return `${decoder.decode(bytes.subarray(0, leftEnd))}${marker}${decoder.decode(bytes.subarray(rightStart))}`;
1146
- }
1147
-
1148
- function settleBrowserEventTruncation(
1149
- event: SessionEvent,
1150
- truncation: {
1151
- originalBytes: number | null;
1152
- deliveredBytes: number;
1153
- omittedBytes: number | null;
1154
- estimatedDeliveredTokens: number;
1155
- },
1156
- ): void {
1157
- for (let attempt = 0; attempt < 16; attempt += 1) {
1158
- const deliveredBytes = browserJsonBytes(event);
1159
- const omittedBytes =
1160
- truncation.originalBytes === null
1161
- ? null
1162
- : Math.max(0, truncation.originalBytes - deliveredBytes);
1163
- const estimatedDeliveredTokens = Math.ceil(deliveredBytes / 4);
1164
- if (
1165
- truncation.deliveredBytes === deliveredBytes &&
1166
- truncation.omittedBytes === omittedBytes &&
1167
- truncation.estimatedDeliveredTokens === estimatedDeliveredTokens
1168
- ) {
1169
- return;
1170
- }
1171
- truncation.deliveredBytes = deliveredBytes;
1172
- truncation.omittedBytes = omittedBytes;
1173
- truncation.estimatedDeliveredTokens = estimatedDeliveredTokens;
1174
- }
1175
- throw new RangeError("Browser event byte accounting did not converge");
1176
- }
1177
-
1178
- function browserUtf8Bytes(value: string): number {
1179
- return encoder.encode(value).byteLength;
1180
- }
1181
-
1182
- function isBrowserUtf8Continuation(value: number): boolean {
1183
- return (value & 0xc0) === 0x80;
924
+ return encoder.encode(JSON.stringify(value)).byteLength;
1184
925
  }
1185
926
 
1186
927
  function observeSessionStatus(
@@ -1240,9 +981,7 @@ async function planForegroundCatchup(
1240
981
  pageSize: rawGap,
1241
982
  ...(options.signal ? { signal: options.signal } : {}),
1242
983
  });
1243
- const events = page
1244
- .filter((event) => eventResumeSequence(event) > cursor)
1245
- .map(boundBrowserLegacyEvent);
984
+ const events = page.filter((event) => eventResumeSequence(event) > cursor);
1246
985
  if (events.length === 0) {
1247
986
  return { kind: "reload" };
1248
987
  }
package/src/index.ts CHANGED
@@ -48,7 +48,6 @@ export {
48
48
  SESSION_EVENT_BROWSER_MAX_COUNT,
49
49
  SESSION_EVENT_BROWSER_PENDING_MAX_BYTES,
50
50
  SESSION_EVENT_BROWSER_PENDING_MAX_COUNT,
51
- SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES,
52
51
  boundBrowserSessionEventWindow,
53
52
  useSessionEvents,
54
53
  } from "./hooks/use-session-events";
@@ -8,6 +8,7 @@
8
8
  */
9
9
  export type OlderHistoryLoadReceipt = Promise<boolean> & {
10
10
  readonly committed: boolean;
11
+ readonly tailPreserved?: boolean;
11
12
  };
12
13
 
13
14
  /**
@@ -22,23 +23,29 @@ export type OlderHistoryLoader = () => unknown;
22
23
 
23
24
  type MutableOlderHistoryLoadReceipt = Promise<boolean> & {
24
25
  committed: boolean;
26
+ tailPreserved: boolean;
25
27
  };
26
28
 
27
29
  type OlderHistoryReceiptCapture = (receipt: OlderHistoryLoadReceipt) => void;
28
30
 
29
31
  let activeReceiptCapture: OlderHistoryReceiptCapture | undefined;
32
+ let preserveTailForAutomaticLoad = false;
30
33
 
31
34
  /** Bind a receipt before a synchronous forwarding stack can publish state. */
32
35
  export function invokeOlderHistoryLoaderWithReceiptCapture(
33
36
  load: OlderHistoryLoader,
34
37
  capture: OlderHistoryReceiptCapture,
38
+ preserveTail = false,
35
39
  ): unknown {
36
40
  const previousCapture = activeReceiptCapture;
41
+ const previousPreserveTail = preserveTailForAutomaticLoad;
37
42
  activeReceiptCapture = capture;
43
+ preserveTailForAutomaticLoad = preserveTail;
38
44
  try {
39
45
  return load();
40
46
  } finally {
41
47
  activeReceiptCapture = previousCapture;
48
+ preserveTailForAutomaticLoad = previousPreserveTail;
42
49
  }
43
50
  }
44
51
 
@@ -47,7 +54,11 @@ export function invokeOlderHistoryLoaderWithReceiptCapture(
47
54
  * Call `markCommitted` immediately before publishing an accepted older window.
48
55
  */
49
56
  export function createOlderHistoryLoadReceipt(
50
- load: (markCommitted: () => void) => boolean | Promise<boolean>,
57
+ load: (
58
+ markCommitted: () => void,
59
+ preserveTail: boolean,
60
+ markTailPreserved: () => void,
61
+ ) => boolean | Promise<boolean>,
51
62
  ): OlderHistoryLoadReceipt {
52
63
  let resolveResult!: (value: boolean | PromiseLike<boolean>) => void;
53
64
  let rejectResult!: (reason?: unknown) => void;
@@ -56,11 +67,18 @@ export function createOlderHistoryLoadReceipt(
56
67
  rejectResult = reject;
57
68
  }) as MutableOlderHistoryLoadReceipt;
58
69
  result.committed = false;
70
+ result.tailPreserved = false;
59
71
  activeReceiptCapture?.(result);
60
72
  try {
61
- const loaded = load(() => {
62
- result.committed = true;
63
- });
73
+ const loaded = load(
74
+ () => {
75
+ result.committed = true;
76
+ },
77
+ preserveTailForAutomaticLoad,
78
+ () => {
79
+ result.tailPreserved = true;
80
+ },
81
+ );
64
82
  Promise.resolve(loaded).then(resolveResult, rejectResult);
65
83
  } catch (error) {
66
84
  rejectResult(error);
@@ -65,7 +65,10 @@ export default function PlatformActivityRow({
65
65
  return j(ActivityDisclosure, {
66
66
  icon: j(BotIcon, { className: "size-3.5" }),
67
67
  iconTone: failed ? "failed" : running ? "running" : "muted",
68
- title: startupPhaseTitle(item.phase, item.status, item.outcome),
68
+ title:
69
+ item.blockedReason === "rotation_in_progress"
70
+ ? "Waiting for sandbox rotation"
71
+ : startupPhaseTitle(item.phase, item.status, item.outcome),
69
72
  preview:
70
73
  item.phase === "model_preparation"
71
74
  ? "Includes overlapping sandbox, rig, repository, and runtime setup shown below."
@@ -135,6 +135,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
135
135
  const ordered = orderTimelineEvents(events, prescan);
136
136
  const pendingWaitOutcomeByTurn = new Map<string | null, PendingWaitOutcome>();
137
137
  const latestAgentResponseByTurn = new Map<string | null, TrackedAgentResponse>();
138
+ const identifiedMessages = new Map<string, AgentMessageItem>();
138
139
  const humanInputRequests = humanInputRequestsById(events);
139
140
  const humanInputToolCallIds = new Set(
140
141
  [...humanInputRequests.values()]
@@ -157,6 +158,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
157
158
  mode = 0,
158
159
  explicitStartedAt?: string,
159
160
  explicitId?: string,
161
+ blockedReason?: StartupPhaseItem["blockedReason"],
160
162
  ): void => {
161
163
  const key = `${startupTurnId}:${phase}`;
162
164
  const priorState = startupPhases.get(key);
@@ -183,6 +185,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
183
185
  ? elapsedDurationMs(open.startedAt, startupEvent.occurredAt)
184
186
  : Math.max(0, duration);
185
187
  open.outcome = outcome ?? open.outcome;
188
+ open.blockedReason = blockedReason;
186
189
  return;
187
190
  }
188
191
  const running = status === "running";
@@ -201,6 +204,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
201
204
  completedAt: running ? null : startupEvent.occurredAt,
202
205
  durationMs,
203
206
  outcome,
207
+ blockedReason,
204
208
  occurredAt: startedAt,
205
209
  } satisfies Partial<StartupPhaseItem>);
206
210
  startupPhases.set(key, [prior, startupAttemptId, startupRecoveryRevision]);
@@ -216,6 +220,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
216
220
  completedAt: running ? null : startupEvent.occurredAt,
217
221
  durationMs,
218
222
  outcome,
223
+ blockedReason,
219
224
  occurredAt: startedAt,
220
225
  };
221
226
  items.push(item);
@@ -408,8 +413,43 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
408
413
  if (!text) {
409
414
  break;
410
415
  }
416
+ const messageId = stringValue(payload.messageId);
417
+ const messageKey = messageId ? JSON.stringify([turnId, messageId]) : null;
418
+ const identified = messageKey ? identifiedMessages.get(messageKey) : undefined;
419
+ if (identified) {
420
+ if (identified.annotationSource?.eventType !== "agent.message.completed") {
421
+ identified.text += text;
422
+ rememberAgentResponse(turnId, identified, false);
423
+ }
424
+ break;
425
+ }
411
426
  const open = last();
412
- if (open?.kind === "agent-message" && open.streaming && open.turnId === turnId) {
427
+ if (!messageKey && open?.kind === "tool-call" && open.status === "running") {
428
+ const previous = latestAgentResponseByTurn.get(turnId);
429
+ const previousIndex = previous ? items.indexOf(previous.item) : -1;
430
+ if (
431
+ previous &&
432
+ !previous.completed &&
433
+ previousIndex >= 0 &&
434
+ items
435
+ .slice(previousIndex + 1)
436
+ .every(
437
+ (item) =>
438
+ item.kind === "tool-call" && item.turnId === turnId && item.status === "running",
439
+ )
440
+ ) {
441
+ // Legacy deltas have no provider message identity. Tool creation
442
+ // alone is not a text boundary; completed output still is.
443
+ previous.item.text += text;
444
+ break;
445
+ }
446
+ }
447
+ if (
448
+ !messageKey &&
449
+ open?.kind === "agent-message" &&
450
+ open.streaming &&
451
+ open.turnId === turnId
452
+ ) {
413
453
  open.text += text;
414
454
  rememberAgentResponse(turnId, open, false);
415
455
  break;
@@ -424,18 +464,21 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
424
464
  occurredAt: event.occurredAt,
425
465
  };
426
466
  items.push(item);
467
+ if (messageKey) identifiedMessages.set(messageKey, item);
427
468
  rememberAgentResponse(turnId, item, false);
428
469
  break;
429
470
  }
430
471
 
431
472
  case "agent.message.completed": {
432
473
  const text = stringValue(payload.text);
474
+ const messageId = stringValue(payload.messageId);
475
+ const messageKey = messageId ? JSON.stringify([turnId, messageId]) : null;
433
476
  const phase = assistantMessagePhase(payload.phase);
434
477
  // Reconcile the most recent same-turn agent message — even when
435
478
  // activity (tool calls, reasoning) landed after its deltas — so the
436
479
  // completed text never duplicates the streamed one.
437
- let openIndex = -1;
438
- for (let index = items.length - 1; index >= 0; index -= 1) {
480
+ let openIndex = messageKey ? items.indexOf(identifiedMessages.get(messageKey)!) : -1;
481
+ for (let index = messageKey ? -1 : items.length - 1; index >= 0; index -= 1) {
439
482
  const candidate = items[index];
440
483
  if (candidate?.kind === "agent-message" && candidate.turnId === turnId) {
441
484
  openIndex = index;
@@ -447,10 +490,15 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
447
490
  candidate?.kind === "agent-message" ? candidate : undefined;
448
491
  if (
449
492
  open &&
450
- (open.streaming || !open.text || text === open.text || text.startsWith(open.text))
493
+ (messageKey ||
494
+ open.streaming ||
495
+ !open.text ||
496
+ text === open.text ||
497
+ text.startsWith(open.text))
451
498
  ) {
452
- // The completed text is authoritative when it extends what streamed.
453
- if (!open.text || (text && text.startsWith(open.text))) {
499
+ // Identity makes the final text authoritative even if it corrects the
500
+ // draft; legacy receipts still require an extension match.
501
+ if (!open.text || (text && (messageKey || text.startsWith(open.text)))) {
454
502
  open.text = text || open.text;
455
503
  }
456
504
  open.streaming = false;
@@ -502,6 +550,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
502
550
  },
503
551
  };
504
552
  items.push(item);
553
+ if (messageKey) identifiedMessages.set(messageKey, item);
505
554
  rememberAgentResponse(turnId, item, true);
506
555
  }
507
556
  break;
@@ -693,7 +742,18 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
693
742
  const startupPhase = startupPhaseForSandboxOperation(name);
694
743
  if (startupPhase) {
695
744
  closeStreamingTail();
696
- settleStartupPhase(startupPhase, status, numberOrNull(payload.durationMs), origin);
745
+ settleStartupPhase(
746
+ startupPhase,
747
+ status,
748
+ numberOrNull(payload.durationMs),
749
+ origin,
750
+ 0,
751
+ undefined,
752
+ undefined,
753
+ status === "cancelled" && payload.failureCode === "rotation_in_progress"
754
+ ? "rotation_in_progress"
755
+ : undefined,
756
+ );
697
757
  break;
698
758
  }
699
759
  // Routine per-turn platform plumbing that runs before EVERY turn to
@@ -1039,11 +1099,17 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
1039
1099
  }
1040
1100
  }
1041
1101
  finalizeOpen(turnId, "complete", event.occurredAt);
1042
- items.push(turnEndItem(event, "complete", null));
1102
+ const completedTurn = turnEndItem(event, "complete", null);
1103
+ items.push(completedTurn);
1043
1104
  const hasCompletedFinalResponse =
1044
1105
  latestAgentResponse?.completed === true &&
1045
1106
  latestAgentResponse.item.phase !== "commentary" &&
1046
1107
  Boolean(visibleTrackedResponse);
1108
+ if (!hasAuthoritativeFinalOutput && pendingWaitOutcome) {
1109
+ // Preserve existing prose, including delta-only streams, when a
1110
+ // trailing wait yields without a terminal output receipt.
1111
+ completedTurn.preserveWaitResponse = true;
1112
+ }
1047
1113
  if (!hasAuthoritativeFinalOutput && !hasCompletedFinalResponse && pendingWaitOutcome) {
1048
1114
  items.push({
1049
1115
  kind: "notice",
@@ -1808,7 +1874,31 @@ function foldSettledTurn(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
1808
1874
  return;
1809
1875
  }
1810
1876
 
1811
- const finalMessage = extractFinalAgentMessage(collected, turnEnd);
1877
+ // Resolve only inside this turn's collected suffix. A user/machine-input
1878
+ // boundary or an interleaved foreign turn must never donate its answer.
1879
+ const waitMessages = turnEnd.preserveWaitResponse
1880
+ ? collected.filter(
1881
+ (group): group is Extract<TimelineGroup, { kind: "item" }> =>
1882
+ group.kind === "item" &&
1883
+ group.item.kind === "agent-message" &&
1884
+ !group.item.streaming &&
1885
+ group.item.text.trim().length > 0 &&
1886
+ belongsToTurn(group.item, turnEnd.turnId),
1887
+ )
1888
+ : [];
1889
+ // A completed answer outranks later commentary. Otherwise use the latest
1890
+ // visible existing prose, ignoring whitespace and opaque-only stream tails.
1891
+ const waitResponse =
1892
+ waitMessages
1893
+ .slice()
1894
+ .reverse()
1895
+ .find(
1896
+ (group) =>
1897
+ group.item.kind === "agent-message" &&
1898
+ group.item.phase !== "commentary" &&
1899
+ group.item.annotationSource?.eventType === "agent.message.completed",
1900
+ ) ?? waitMessages.at(-1);
1901
+ const finalMessage = waitResponse ?? extractFinalAgentMessage(collected, turnEnd);
1812
1902
  const fallbackMessage =
1813
1903
  finalMessage || hasOrdinaryFinalAgentMessage(collected, turnEnd)
1814
1904
  ? null
@@ -215,6 +215,7 @@ export type StartupPhaseItem = {
215
215
  durationMs: number | null;
216
216
  /** Sandbox origin and rig marker outcomes refine the settled label only. */
217
217
  outcome: "created" | "restored" | "resumed" | "skipped" | null;
218
+ blockedReason?: "rotation_in_progress" | undefined;
218
219
  occurredAt: string;
219
220
  };
220
221
 
@@ -451,6 +452,8 @@ export type TurnOutcome = "complete" | "failed" | "cancelled";
451
452
 
452
453
  export type TurnEndItem = {
453
454
  kind: "turn-end";
455
+ /** Keep an existing answer visible when a recorded input wait ends without final output. */
456
+ preserveWaitResponse?: true;
454
457
  id: string;
455
458
  turnId: string | null;
456
459
  outcome: TurnOutcome;