@opengeni/react 3.8.0-canary.0 → 4.0.1-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +11 -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-5M6VTFJ5.js +171 -0
  6. package/dist/chunk-5M6VTFJ5.js.map +1 -0
  7. package/dist/{chunk-IOLVN74D.js → chunk-6J56T3MG.js} +15 -1
  8. package/dist/chunk-6J56T3MG.js.map +1 -0
  9. package/dist/{chunk-ELIYWBZR.js → chunk-BV5A4OTE.js} +4 -4
  10. package/dist/{chunk-3HIF5IFC.js → chunk-HO6X4FHG.js} +2597 -1030
  11. package/dist/chunk-HO6X4FHG.js.map +1 -0
  12. package/dist/{chunk-BXJOQFCD.js → chunk-HUMIRRYI.js} +84 -221
  13. package/dist/chunk-HUMIRRYI.js.map +1 -0
  14. package/dist/{chunk-ZFOFY5ST.js → chunk-NLTS7JHI.js} +16 -31
  15. package/dist/chunk-NLTS7JHI.js.map +1 -0
  16. package/dist/chunk-NOSYBS2D.js +208 -0
  17. package/dist/chunk-NOSYBS2D.js.map +1 -0
  18. package/dist/chunk-YBM6CQRU.js +292 -0
  19. package/dist/chunk-YBM6CQRU.js.map +1 -0
  20. package/dist/{chunk-BQGIXRKV.js → chunk-Z7WBDB4E.js} +416 -290
  21. package/dist/chunk-Z7WBDB4E.js.map +1 -0
  22. package/dist/chunk-ZYRGEKWS.js +216 -0
  23. package/dist/chunk-ZYRGEKWS.js.map +1 -0
  24. package/dist/components/composer.d.ts +4 -2
  25. package/dist/components/human-input-form.d.ts +3 -1
  26. package/dist/components/human-input-surface.d.ts +3 -2
  27. package/dist/components/message-timeline.d.ts +9 -2
  28. package/dist/components/session-conversation.d.ts +2 -0
  29. package/dist/components/timeline-annotation-chrome.d.ts +25 -0
  30. package/dist/components/timeline-annotation-layout.d.ts +42 -0
  31. package/dist/components/timeline-annotation-markers.d.ts +8 -0
  32. package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
  33. package/dist/components/timeline-annotation-shared.d.ts +31 -0
  34. package/dist/components/timeline-annotations-dialog.d.ts +1 -1
  35. package/dist/components/timeline-annotations.d.ts +8 -2
  36. package/dist/composer.js +3 -4
  37. package/dist/{computer-viewer-FWEZISW4.js → computer-viewer-7TEZC6Y2.js} +3 -3
  38. package/dist/hooks/use-codex-accounts.d.ts +11 -1
  39. package/dist/hooks/use-composer.d.ts +2 -0
  40. package/dist/hooks/use-session-events.d.ts +2 -2
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.js +50 -37
  43. package/dist/index.js.map +1 -1
  44. package/dist/interaction.js +3 -3
  45. package/dist/machines.js +1 -1
  46. package/dist/older-history.d.ts +3 -2
  47. package/dist/{platform-activity-row-BJHUHXAT.js → platform-activity-row-27LKAWZD.js} +2 -2
  48. package/dist/platform-activity-row-27LKAWZD.js.map +1 -0
  49. package/dist/{queue-surface-implementation-XL2A6PBM.js → queue-surface-implementation-LK3S4BIW.js} +5 -2
  50. package/dist/{queue-surface-implementation-XL2A6PBM.js.map → queue-surface-implementation-LK3S4BIW.js.map} +1 -1
  51. package/dist/realtime.js +3 -3
  52. package/dist/session-ui.js +11 -11
  53. package/dist/session.js +2 -2
  54. package/dist/timeline/types.d.ts +3 -0
  55. package/dist/timeline-annotation-markers-DRSTLEM6.js +165 -0
  56. package/dist/timeline-annotation-markers-DRSTLEM6.js.map +1 -0
  57. package/dist/timeline-annotation-selection-IHLUGVND.js +236 -0
  58. package/dist/timeline-annotation-selection-IHLUGVND.js.map +1 -0
  59. package/dist/timeline-annotations-dialog-6Q53N5FG.js +262 -0
  60. package/dist/timeline-annotations-dialog-6Q53N5FG.js.map +1 -0
  61. package/package.json +2 -6
  62. package/src/components/chat-composer.tsx +5 -2
  63. package/src/components/composer.tsx +40 -5
  64. package/src/components/copy-button.tsx +1 -0
  65. package/src/components/human-input-form.tsx +185 -53
  66. package/src/components/human-input-surface.tsx +3 -0
  67. package/src/components/machines-dashboard.tsx +1 -16
  68. package/src/components/message-timeline.tsx +397 -302
  69. package/src/components/session-commands-panel.tsx +7 -1
  70. package/src/components/session-conversation.tsx +4 -1
  71. package/src/components/timeline-annotation-chrome.tsx +183 -0
  72. package/src/components/timeline-annotation-layout.ts +268 -0
  73. package/src/components/timeline-annotation-markers.tsx +185 -0
  74. package/src/components/timeline-annotation-reveal-context.ts +9 -0
  75. package/src/components/timeline-annotation-selection.tsx +99 -54
  76. package/src/components/timeline-annotation-shared.ts +245 -0
  77. package/src/components/timeline-annotations-dialog.tsx +172 -128
  78. package/src/components/timeline-annotations.tsx +178 -45
  79. package/src/hooks/use-codex-accounts.ts +32 -5
  80. package/src/hooks/use-composer.ts +17 -0
  81. package/src/hooks/use-session-events.ts +16 -277
  82. package/src/index.ts +0 -1
  83. package/src/older-history.ts +22 -4
  84. package/src/timeline/platform-activity-row.tsx +4 -1
  85. package/src/timeline/projection.ts +99 -9
  86. package/src/timeline/types.ts +3 -0
  87. package/styles/compiled.css +86 -22
  88. package/dist/chat.d.ts +0 -37
  89. package/dist/chat.js +0 -315
  90. package/dist/chat.js.map +0 -1
  91. package/dist/chunk-3HIF5IFC.js.map +0 -1
  92. package/dist/chunk-BQGIXRKV.js.map +0 -1
  93. package/dist/chunk-BXJOQFCD.js.map +0 -1
  94. package/dist/chunk-F2CFHN3Y.js +0 -1415
  95. package/dist/chunk-F2CFHN3Y.js.map +0 -1
  96. package/dist/chunk-HQV2I5HV.js +0 -124
  97. package/dist/chunk-HQV2I5HV.js.map +0 -1
  98. package/dist/chunk-IOLVN74D.js.map +0 -1
  99. package/dist/chunk-W3OUAH6M.js +0 -170
  100. package/dist/chunk-W3OUAH6M.js.map +0 -1
  101. package/dist/chunk-ZFOFY5ST.js.map +0 -1
  102. package/dist/platform-activity-row-BJHUHXAT.js.map +0 -1
  103. package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
  104. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
  105. package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
  106. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
  107. package/src/chat.tsx +0 -393
  108. /package/dist/{browser-viewer-SS46LUAC.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
  109. /package/dist/{chunk-VEPNNDNG.js.map → chunk-24M4YBCL.js.map} +0 -0
  110. /package/dist/{chunk-ELIYWBZR.js.map → chunk-BV5A4OTE.js.map} +0 -0
  111. /package/dist/{computer-viewer-FWEZISW4.js.map → computer-viewer-7TEZC6Y2.js.map} +0 -0
@@ -15,7 +15,16 @@ import {
15
15
 
16
16
  /** Events that change which Codex account a session runs on (or just ran). */
17
17
  export function isCodexAccountEvent(event: Pick<SessionEvent, "type">): boolean {
18
- return event.type === "codex.account.switched";
18
+ return [
19
+ "codex.account.switched",
20
+ "codex.account.selection.changed",
21
+ "codex.capacity.waiting",
22
+ "codex.capacity.resumed",
23
+ "codex.capacity.superseded",
24
+ "turn.completed",
25
+ "turn.failed",
26
+ "turn.cancelled",
27
+ ].includes(event.type);
19
28
  }
20
29
 
21
30
  /**
@@ -30,12 +39,16 @@ export type CodexAccountsClientLike = {
30
39
  getSession?: (
31
40
  workspaceId: string,
32
41
  sessionId: string,
33
- ) => Promise<{ codexPinnedCredentialId?: string | null; codexLastCredentialId?: string | null }>;
42
+ ) => Promise<{
43
+ codexPinnedCredentialId?: string | null;
44
+ codexLastCredentialId?: string | null;
45
+ codexCurrentSelection?: { credentialId: string | null; waiting: boolean } | null;
46
+ }>;
34
47
  pinSessionCodexAccount?: (
35
48
  workspaceId: string,
36
49
  sessionId: string,
37
50
  target: string,
38
- ) => Promise<{ pinned: string }>;
51
+ ) => Promise<{ pinned: string; appliedTo?: "waiting_turn" | "next_turn" }>;
39
52
  /** Optional (absent ⇒ the card hides live refresh): batched live /wham/usage refresh. */
40
53
  refreshCodexUsage?: (workspaceId: string) => Promise<{ usage: Record<string, unknown> }>;
41
54
  };
@@ -55,8 +68,10 @@ export type UseCodexAccountsResult = {
55
68
  activeAccountId: string | null;
56
69
  /** The session's PINNED account (null ⇒ following workspace active). */
57
70
  pinnedAccountId: string | null;
58
- /** The account the next turn will run on: pin > workspace active. */
71
+ /** Current preference only; automatic allocation can choose another account. */
59
72
  effectiveAccountId: string | null;
73
+ currentSelection: { credentialId: string | null; waiting: boolean } | null;
74
+ switchAppliedTo: "waiting_turn" | "next_turn" | null;
60
75
  /** The account the session's last turn ACTUALLY ran on (the "Running on:" source). */
61
76
  lastAccountId: string | null;
62
77
  settings: CodexRotationSettings;
@@ -85,6 +100,7 @@ const EMPTY_SETTINGS: CodexRotationSettings = {
85
100
  };
86
101
 
87
102
  type CodexAccountsState = {
103
+ currentSelection: { credentialId: string | null; waiting: boolean } | null;
88
104
  accounts: CodexAccount[];
89
105
  activeAccountId: string | null;
90
106
  settings: CodexRotationSettings;
@@ -93,6 +109,7 @@ type CodexAccountsState = {
93
109
  };
94
110
 
95
111
  const EMPTY_STATE: CodexAccountsState = {
112
+ currentSelection: null,
96
113
  accounts: [],
97
114
  activeAccountId: null,
98
115
  settings: EMPTY_SETTINGS,
@@ -121,6 +138,7 @@ export function useCodexAccounts(options: UseCodexAccountsOptions = {}): UseCode
121
138
  : Promise.resolve(null);
122
139
  const [acc, session] = await Promise.all([accountsP, sessionP]);
123
140
  return {
141
+ currentSelection: session?.codexCurrentSelection ?? null,
124
142
  accounts: acc.accounts,
125
143
  activeAccountId: acc.activeAccountId,
126
144
  settings: acc.settings,
@@ -140,6 +158,10 @@ export function useCodexAccounts(options: UseCodexAccountsOptions = {}): UseCode
140
158
  const { run: runMutation, mutating: pinning, mutationError } = useMutationRunner();
141
159
  const { run: runUsageMutation, mutating: refreshingUsage } = useMutationRunner();
142
160
  const [pinningTarget, setPinningTarget] = useState<string | null>(null);
161
+ const [switchReceipt, setSwitchReceipt] = useState<{
162
+ sessionId: string;
163
+ appliedTo: "waiting_turn" | "next_turn";
164
+ } | null>(null);
143
165
 
144
166
  // Refresh only after the durable post-selection event. `turn.started` is
145
167
  // emitted before account selection settles and races this authoritative read.
@@ -161,8 +183,10 @@ export function useCodexAccounts(options: UseCodexAccountsOptions = {}): UseCode
161
183
  return false;
162
184
  }
163
185
  setPinningTarget(target);
186
+ setSwitchReceipt(null);
164
187
  const result = await runMutation(async () => {
165
- await codexClient.pinSessionCodexAccount!(workspaceId, sessionId, target);
188
+ const receipt = await codexClient.pinSessionCodexAccount!(workspaceId, sessionId, target);
189
+ setSwitchReceipt({ sessionId, appliedTo: receipt.appliedTo ?? "next_turn" });
166
190
  return true;
167
191
  });
168
192
  setPinningTarget(null);
@@ -191,6 +215,9 @@ export function useCodexAccounts(options: UseCodexAccountsOptions = {}): UseCode
191
215
  const effectiveAccountId = data.pinnedAccountId ?? data.activeAccountId;
192
216
 
193
217
  return {
218
+ currentSelection: data.currentSelection,
219
+ switchAppliedTo:
220
+ switchReceipt?.sessionId === sessionId ? (switchReceipt?.appliedTo ?? null) : null,
194
221
  accounts: data.accounts,
195
222
  activeAccountId: data.activeAccountId,
196
223
  pinnedAccountId: data.pinnedAccountId,
@@ -424,6 +424,8 @@ export type ComposerState = {
424
424
  /** Newly captured annotation the review surface should focus. */
425
425
  annotationReviewTargetId?: string | null | undefined;
426
426
  clearAnnotationReviewTarget?: (() => void) | undefined;
427
+ /** Focus an annotation, or the first incomplete note when omitted. */
428
+ requestAnnotationReview?: ((id?: string) => void) | undefined;
427
429
  /** Read the current draft synchronously before a destructive replacement. */
428
430
  hasDraftContent: () => boolean;
429
431
  /** Append the draft behind prompts already visible in the queue. */
@@ -2458,6 +2460,20 @@ export function useComposer(
2458
2460
  setAnnotationReviewTargetId(null);
2459
2461
  }, []);
2460
2462
 
2463
+ const requestAnnotationReview = useCallback((id?: string) => {
2464
+ const current = annotationsRef.current;
2465
+ if (current.length === 0) {
2466
+ setAnnotationReviewTargetId(null);
2467
+ return;
2468
+ }
2469
+ if (id && current.some((annotation) => annotation.id === id)) {
2470
+ setAnnotationReviewTargetId(id);
2471
+ return;
2472
+ }
2473
+ const incomplete = current.find((annotation) => annotation.note.trim().length === 0);
2474
+ setAnnotationReviewTargetId(incomplete?.id ?? current[0]!.id);
2475
+ }, []);
2476
+
2461
2477
  const updatePolicy = useCallback(
2462
2478
  (next: ComposerPolicy): void => {
2463
2479
  if (targetKeyRef.current !== targetKey) return;
@@ -2643,6 +2659,7 @@ export function useComposer(
2643
2659
  removeAnnotation,
2644
2660
  annotationReviewTargetId: identityMatches ? annotationReviewTargetId : null,
2645
2661
  clearAnnotationReviewTarget,
2662
+ requestAnnotationReview,
2646
2663
  hasDraftContent,
2647
2664
  send,
2648
2665
  optimisticMessages: visibleOptimisticMessages,
@@ -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."