@opengeni/react 0.57.0 → 1.0.2-canary.37191796234001
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +654 -22
- package/dist/accounts.d.ts +64 -0
- package/dist/accounts.js +822 -0
- package/dist/accounts.js.map +1 -0
- package/dist/approvals.d.ts +2 -1
- package/dist/artifacts-document.js +3 -2
- package/dist/artifacts-presentation.js +3 -2
- package/dist/artifacts-spreadsheet.js +2 -2
- package/dist/artifacts.d.ts +4 -1
- package/dist/artifacts.js +518 -42
- package/dist/artifacts.js.map +1 -1
- package/dist/browser-viewer-GTR5XQ7X.js +16 -0
- package/dist/capability-catalog-row.d.ts +20 -0
- package/dist/capability-logo-fallback.d.ts +3 -0
- package/dist/chat-interactive-block-UGBTNB3L.js +11 -0
- package/dist/chat-interactive-block-UGBTNB3L.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-23J2ZNXT.js} +4117 -3896
- package/dist/chunk-23J2ZNXT.js.map +1 -0
- package/dist/chunk-447JLCBU.js +209 -0
- package/dist/chunk-447JLCBU.js.map +1 -0
- package/dist/chunk-4RVYOY4O.js +379 -0
- package/dist/chunk-4RVYOY4O.js.map +1 -0
- package/dist/chunk-5CVXZA3Y.js +292 -0
- package/dist/chunk-5CVXZA3Y.js.map +1 -0
- package/dist/chunk-5J4FOW6Q.js +108 -0
- package/dist/chunk-5J4FOW6Q.js.map +1 -0
- package/dist/chunk-A2PI6UMY.js +41 -0
- package/dist/chunk-A2PI6UMY.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-A4UJB2MM.js} +37 -27
- package/dist/chunk-A4UJB2MM.js.map +1 -0
- package/dist/chunk-B3V6TG77.js +41 -0
- package/dist/chunk-B3V6TG77.js.map +1 -0
- package/dist/chunk-BRM6FVZA.js +302 -0
- package/dist/chunk-BRM6FVZA.js.map +1 -0
- package/dist/chunk-C5X5AJXK.js +182 -0
- package/dist/chunk-C5X5AJXK.js.map +1 -0
- package/dist/chunk-CR7U7HAR.js +453 -0
- package/dist/chunk-CR7U7HAR.js.map +1 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-DYWBUX5Q.js} +29 -123
- package/dist/chunk-DYWBUX5Q.js.map +1 -0
- package/dist/chunk-E3G7SAYZ.js +3124 -0
- package/dist/chunk-E3G7SAYZ.js.map +1 -0
- package/dist/chunk-ELCE5SXV.js +359 -0
- package/dist/chunk-ELCE5SXV.js.map +1 -0
- package/dist/chunk-ERGYJIBB.js +21 -0
- package/dist/chunk-ERGYJIBB.js.map +1 -0
- package/dist/chunk-FFA2OVOV.js +319 -0
- package/dist/chunk-FFA2OVOV.js.map +1 -0
- package/dist/chunk-FZCP527Y.js +116 -0
- package/dist/chunk-FZCP527Y.js.map +1 -0
- package/dist/chunk-GIMRELBW.js +14 -0
- package/dist/chunk-GIMRELBW.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-H2NJQORO.js} +1069 -79
- package/dist/chunk-H2NJQORO.js.map +1 -0
- package/dist/chunk-I7IWRQLN.js +1608 -0
- package/dist/chunk-I7IWRQLN.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-IQUCRAT7.js} +792 -537
- package/dist/chunk-IQUCRAT7.js.map +1 -0
- package/dist/chunk-IWIDV7SB.js +1 -0
- package/dist/chunk-IWIDV7SB.js.map +1 -0
- package/dist/chunk-J2QWVSQY.js +312 -0
- package/dist/chunk-J2QWVSQY.js.map +1 -0
- package/dist/chunk-JFNEV4LU.js +2733 -0
- package/dist/chunk-JFNEV4LU.js.map +1 -0
- package/dist/chunk-JT4JW7BC.js +54 -0
- package/dist/chunk-JT4JW7BC.js.map +1 -0
- package/dist/chunk-K3HQAKQX.js +24 -0
- package/dist/chunk-K3HQAKQX.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-NZUI76EM.js +371 -0
- package/dist/chunk-NZUI76EM.js.map +1 -0
- package/dist/{chunk-PEI6RANI.js → chunk-PNY3JKBJ.js} +309 -74
- package/dist/chunk-PNY3JKBJ.js.map +1 -0
- package/dist/{chunk-IGC6A2ZI.js → chunk-Q5QBEZIQ.js} +777 -246
- package/dist/chunk-Q5QBEZIQ.js.map +1 -0
- package/dist/chunk-RHREZ7BO.js +11 -0
- package/dist/chunk-RHREZ7BO.js.map +1 -0
- package/dist/chunk-SCOTPE2I.js +589 -0
- package/dist/chunk-SCOTPE2I.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-SJZDKVW2.js} +1331 -1163
- package/dist/chunk-SJZDKVW2.js.map +1 -0
- package/dist/chunk-TMJRG4YC.js +12803 -0
- package/dist/chunk-TMJRG4YC.js.map +1 -0
- package/dist/chunk-TMYGL6X5.js +742 -0
- package/dist/chunk-TMYGL6X5.js.map +1 -0
- package/dist/chunk-UGLEYEMP.js +33 -0
- package/dist/chunk-UGLEYEMP.js.map +1 -0
- package/dist/chunk-UK4TINKT.js +27 -0
- package/dist/chunk-UK4TINKT.js.map +1 -0
- package/dist/chunk-UTRTG6LH.js +220 -0
- package/dist/chunk-UTRTG6LH.js.map +1 -0
- package/dist/chunk-WEHMPOXK.js +39 -0
- package/dist/chunk-WEHMPOXK.js.map +1 -0
- package/dist/chunk-WRHIZYZF.js +988 -0
- package/dist/chunk-WRHIZYZF.js.map +1 -0
- package/dist/{chunk-HUIKIDM5.js → chunk-WUISZ3WU.js} +223 -115
- package/dist/chunk-WUISZ3WU.js.map +1 -0
- package/dist/chunk-XCHWDJ22.js +107 -0
- package/dist/chunk-XCHWDJ22.js.map +1 -0
- package/dist/chunk-XOAMRDDK.js +481 -0
- package/dist/chunk-XOAMRDDK.js.map +1 -0
- package/dist/chunk-XUW2UWQM.js +299 -0
- package/dist/chunk-XUW2UWQM.js.map +1 -0
- package/dist/{chunk-KCGXPP5V.js → chunk-Y2PRMYLC.js} +11 -6
- package/dist/chunk-Y2PRMYLC.js.map +1 -0
- package/dist/chunk-YBXQDQFX.js +1772 -0
- package/dist/chunk-YBXQDQFX.js.map +1 -0
- package/dist/{chunk-KZ73RL76.js → chunk-YQEAA6LT.js} +7 -3
- package/dist/chunk-YQEAA6LT.js.map +1 -0
- package/dist/chunk-YV4L3ILF.js +9 -0
- package/dist/chunk-YV4L3ILF.js.map +1 -0
- package/dist/{chunk-LLOLUHVG.js → chunk-YWFG7ZX7.js} +1358 -2746
- package/dist/chunk-YWFG7ZX7.js.map +1 -0
- package/dist/chunk-ZYRGEKWS.js +216 -0
- package/dist/chunk-ZYRGEKWS.js.map +1 -0
- package/dist/client.d.ts +7 -4
- package/dist/clipboard.js +1 -0
- package/dist/commands/types.d.ts +2 -0
- package/dist/components/anthropic-mark.d.ts +3 -0
- package/dist/components/approval-surface.d.ts +15 -0
- package/dist/components/artifacts/artifact-chrome.d.ts +120 -0
- package/dist/components/artifacts/artifact-sandbox.d.ts +27 -0
- package/dist/components/artifacts/artifact-surface.d.ts +3 -1
- package/dist/components/artifacts/chat-interactive-block.d.ts +29 -0
- package/dist/components/artifacts/deferred-chat-media.d.ts +15 -0
- package/dist/components/artifacts/document-editor.d.ts +4 -2
- package/dist/components/artifacts/editable-artifact-ui.d.ts +2 -1
- package/dist/components/artifacts/editable-artifact-view.d.ts +46 -0
- package/dist/components/artifacts/editable-document.d.ts +1 -1
- package/dist/components/artifacts/editable-presentation.d.ts +2 -2
- package/dist/components/artifacts/editable-spreadsheet.d.ts +2 -1
- package/dist/components/artifacts/index.d.ts +8 -1
- package/dist/components/artifacts/inline-html-document.d.ts +2 -0
- package/dist/components/artifacts/latest-task-runner.d.ts +15 -0
- package/dist/components/artifacts/presentation-editor.d.ts +6 -2
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +48 -3
- package/dist/components/artifacts/session-artifact-viewer.d.ts +39 -0
- package/dist/components/artifacts/site-view.d.ts +23 -0
- package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
- package/dist/components/artifacts/visualization-assets.gen.d.ts +2 -0
- package/dist/components/browser-select-control.d.ts +16 -0
- package/dist/components/browser-viewer.d.ts +10 -1
- package/dist/components/chat-composer.d.ts +21 -3
- package/dist/components/child-session-link.d.ts +5 -0
- package/dist/components/claude-mark.d.ts +3 -0
- package/dist/components/code-editor.d.ts +1 -4
- package/dist/components/composer.d.ts +45 -4
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/copy-button.d.ts +3 -1
- package/dist/components/desktop-viewer.d.ts +1 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +10 -1
- package/dist/components/fleet-tile.d.ts +2 -2
- package/dist/components/grok-mark.d.ts +26 -0
- package/dist/components/human-input-form.d.ts +21 -2
- package/dist/components/human-input-surface.d.ts +6 -3
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown-table-layout.d.ts +12 -0
- package/dist/components/markdown.d.ts +47 -3
- package/dist/components/message-timeline.d.ts +72 -9
- package/dist/components/model-mark.d.ts +29 -0
- package/dist/components/model-policy-picker-menu.d.ts +9 -0
- package/dist/components/model-policy-picker.d.ts +62 -13
- package/dist/components/open-geni-chat.d.ts +55 -0
- package/dist/components/open-geni-links.d.ts +68 -0
- package/dist/components/openrouter-mark.d.ts +6 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/preview-loading.d.ts +2 -0
- package/dist/components/queue-item-content.d.ts +9 -0
- package/dist/components/sandbox-files.d.ts +13 -2
- package/dist/components/sandbox-workspace.d.ts +43 -3
- package/dist/components/session-capability-frame.d.ts +35 -0
- package/dist/components/session-chrome.d.ts +66 -8
- package/dist/components/session-commands-panel.d.ts +6 -0
- package/dist/components/session-commands.d.ts +6 -0
- package/dist/components/session-connection-request.d.ts +8 -0
- package/dist/components/session-conversation.d.ts +86 -0
- package/dist/components/session-list.d.ts +36 -0
- package/dist/components/session-mcp-capability-card.d.ts +41 -0
- package/dist/components/timeline-anchor.d.ts +24 -0
- package/dist/components/timeline-annotation-chrome.d.ts +25 -0
- package/dist/components/timeline-annotation-layout.d.ts +42 -0
- package/dist/components/timeline-annotation-markers.d.ts +8 -0
- package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
- package/dist/components/timeline-annotation-shared.d.ts +31 -0
- package/dist/components/timeline-annotations-dialog.d.ts +3 -2
- package/dist/components/timeline-annotations.d.ts +13 -3
- package/dist/components/timeline-reading-progress.d.ts +11 -0
- package/dist/components/timeline-search.d.ts +34 -0
- package/dist/components/timeline-settlement.d.ts +16 -0
- package/dist/components/tip-follow.d.ts +16 -0
- package/dist/components/usage-limit-notice.d.ts +49 -0
- package/dist/components/usage-member-list.d.ts +91 -0
- package/dist/components/usage-meter.d.ts +73 -0
- package/dist/components/use-viewer-menu-dismiss.d.ts +2 -0
- package/dist/components/user-message-body.d.ts +10 -1
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.d.ts +5 -4
- package/dist/composer.js +15 -9
- package/dist/computer-viewer-ENHVAZ2Q.js +19 -0
- package/dist/computer-viewer-ENHVAZ2Q.js.map +1 -0
- package/dist/connect-account-identity.d.ts +8 -0
- package/dist/connect-accounts.d.ts +15 -0
- package/dist/connect-chooser.d.ts +18 -0
- package/dist/connect-panel.d.ts +18 -0
- package/dist/connect-setup.d.ts +16 -0
- package/dist/connect.d.ts +21 -0
- package/dist/connect.js +2129 -0
- package/dist/connect.js.map +1 -0
- package/dist/connection-catalog.d.ts +38 -0
- package/dist/connection-discovery.d.ts +12 -0
- package/dist/connection-installed.d.ts +17 -0
- package/dist/connection-logo.d.ts +9 -0
- package/dist/connection-service-logo.d.ts +8 -0
- package/dist/connection-service-presentation.d.ts +13 -0
- package/dist/connection-skeleton.d.ts +5 -0
- package/dist/connection-type-picker.d.ts +6 -0
- package/dist/conversation-timeline.d.ts +5 -0
- package/dist/desktop.d.ts +7 -0
- package/dist/desktop.js +26 -0
- package/dist/desktop.js.map +1 -0
- package/dist/device-authorization.d.ts +23 -0
- package/dist/diffs.d.ts +8 -0
- package/dist/diffs.js +14 -0
- package/dist/diffs.js.map +1 -0
- package/dist/discovery-cache.d.ts +5 -0
- package/dist/editor.d.ts +7 -0
- package/dist/editor.js +21 -0
- package/dist/editor.js.map +1 -0
- package/dist/embedded-session-client.d.ts +23 -0
- package/dist/file-node-visibility.d.ts +15 -0
- package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-YZ3ZKWMM.js} +2 -1
- package/dist/fleet-decision-row-YZ3ZKWMM.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/latest-question.d.ts +25 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-browser-frame-stream.d.ts +1 -1
- package/dist/hooks/use-browser-session.d.ts +12 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-codex-accounts.d.ts +25 -7
- package/dist/hooks/use-composer.d.ts +45 -9
- package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
- package/dist/hooks/use-computer-input-posture.d.ts +13 -0
- package/dist/hooks/use-computer-session.d.ts +2 -0
- package/dist/hooks/use-connect.d.ts +22 -0
- package/dist/hooks/use-desktop-stream.d.ts +1 -1
- package/dist/hooks/use-file-attachments.d.ts +21 -2
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +5 -1
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-session-events.d.ts +30 -8
- package/dist/hooks/use-session-lineage.d.ts +4 -0
- package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
- package/dist/hooks/use-session.d.ts +6 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-usage.d.ts +40 -0
- package/dist/hooks/use-workspace-edit.d.ts +4 -0
- package/dist/hooks/use-workspace-sessions.d.ts +27 -5
- package/dist/human-input.d.ts +3 -1
- package/dist/identity-link-accounts.d.ts +11 -0
- package/dist/identity-link-consent.d.ts +18 -0
- package/dist/index.d.ts +60 -17
- package/dist/index.js +2237 -6200
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +1 -1
- package/dist/interaction.js +35 -184
- package/dist/interaction.js.map +1 -1
- package/dist/latest-question-755OW5DS.js +117 -0
- package/dist/latest-question-755OW5DS.js.map +1 -0
- package/dist/lib/error-message.d.ts +7 -0
- package/dist/lib/format.d.ts +36 -2
- package/dist/lib/host-theme.d.ts +70 -0
- package/dist/lib/interaction-errors.d.ts +6 -0
- package/dist/lib/menu-styles.d.ts +22 -0
- package/dist/lib/pierre-diffs-loader.d.ts +20 -0
- package/dist/lib/workbench-peers.d.ts +36 -0
- package/dist/lib/xterm-theme.d.ts +7 -5
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +14 -4
- package/dist/markdown-table-layout-QXSLK4BY.js +106 -0
- package/dist/markdown-table-layout-QXSLK4BY.js.map +1 -0
- package/dist/mcp-connection-status.d.ts +14 -0
- package/dist/model-policy-picker-menu-PWRI7R2X.js +14 -0
- package/dist/model-policy-picker-menu-PWRI7R2X.js.map +1 -0
- package/dist/model-policy.d.ts +19 -3
- package/dist/model-policy.js +9 -1
- package/dist/older-history.d.ts +30 -0
- package/dist/platform-activity-row-627BFZ46.js +182 -0
- package/dist/platform-activity-row-627BFZ46.js.map +1 -0
- package/dist/plugin-details.d.ts +14 -0
- package/dist/plugin-discovery.d.ts +25 -0
- package/dist/provider.d.ts +13 -1
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-ZHFI5JBP.js} +131 -44
- package/dist/queue-surface-implementation-ZHFI5JBP.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +10 -3
- package/dist/realtime.js +229 -83
- package/dist/realtime.js.map +1 -1
- package/dist/sandbox-files-BBC47SLD.js +20 -0
- package/dist/sandbox-files-BBC47SLD.js.map +1 -0
- package/dist/session-auth-recommendation.d.ts +5 -0
- package/dist/session-capability-policy.d.ts +12 -0
- package/dist/session-ui.d.ts +30 -2
- package/dist/session-ui.js +85 -7
- package/dist/session-ui.js.map +1 -1
- package/dist/session.d.ts +8 -2
- package/dist/session.js +30 -19
- package/dist/sites-ui.d.ts +34 -0
- package/dist/sites.d.ts +3 -0
- package/dist/sites.js +11 -0
- package/dist/sites.js.map +1 -0
- package/dist/skill-discovery.d.ts +47 -0
- package/dist/terminal.d.ts +8 -0
- package/dist/terminal.js +31 -0
- package/dist/terminal.js.map +1 -0
- package/dist/timeline/activity-rail.d.ts +11 -2
- package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
- package/dist/timeline/compaction-copy.d.ts +10 -0
- package/dist/timeline/entrance.d.ts +13 -1
- package/dist/timeline/genie-loading.d.ts +34 -0
- package/dist/timeline/index.d.ts +6 -2
- package/dist/timeline/knowledge-receipt.d.ts +17 -0
- package/dist/timeline/platform-activity-row.d.ts +14 -0
- package/dist/timeline/presented-image.d.ts +6 -0
- package/dist/timeline/projection.d.ts +31 -2
- package/dist/timeline/registry.d.ts +2 -0
- package/dist/timeline/retained-image.d.ts +18 -0
- package/dist/timeline/rolling-activity.d.ts +11 -0
- package/dist/timeline/screenshot-lightbox.d.ts +16 -1
- package/dist/timeline/shared.d.ts +8 -3
- package/dist/timeline/startup-preference.d.ts +3 -0
- package/dist/timeline/startup-timings.d.ts +4 -0
- package/dist/timeline/tool-display-name.d.ts +2 -1
- package/dist/timeline/turn-summary.d.ts +32 -3
- package/dist/timeline/types.d.ts +141 -7
- package/dist/timeline-annotation-markers-GNDHTGBD.js +165 -0
- package/dist/timeline-annotation-markers-GNDHTGBD.js.map +1 -0
- package/dist/timeline-annotation-selection-DMLMM2TX.js +241 -0
- package/dist/timeline-annotation-selection-DMLMM2TX.js.map +1 -0
- package/dist/timeline-annotations-dialog-ELRDBHTS.js +273 -0
- package/dist/timeline-annotations-dialog-ELRDBHTS.js.map +1 -0
- package/dist/types/machines.d.ts +2 -2
- package/dist/usage/allowance-copy.d.ts +25 -0
- package/dist/usage/summary.d.ts +35 -0
- package/dist/usage.d.ts +13 -0
- package/dist/usage.js +1074 -0
- package/dist/usage.js.map +1 -0
- package/package.json +44 -4
- package/src/accounts.tsx +1072 -0
- package/src/approvals.ts +8 -1
- package/src/artifacts.ts +42 -0
- package/src/assets/README.md +8 -0
- package/src/assets/gmail.ico +0 -0
- package/src/assets/outlook.ico +0 -0
- package/src/capability-catalog-row.tsx +94 -0
- package/src/capability-logo-fallback.ts +22 -0
- package/src/client.ts +22 -13
- package/src/commands/registry.ts +18 -15
- package/src/commands/types.ts +2 -0
- package/src/components/anthropic-mark.tsx +10 -0
- package/src/components/approval-surface.tsx +171 -37
- package/src/components/artifacts/artifact-chrome.tsx +413 -0
- package/src/components/artifacts/artifact-sandbox.tsx +185 -0
- package/src/components/artifacts/artifact-surface.tsx +39 -19
- package/src/components/artifacts/chat-interactive-block.tsx +260 -0
- package/src/components/artifacts/deferred-chat-media.tsx +67 -0
- package/src/components/artifacts/document-editor.tsx +141 -16
- package/src/components/artifacts/editable-artifact-ui.tsx +4 -2
- package/src/components/artifacts/editable-artifact-view.tsx +187 -0
- package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
- package/src/components/artifacts/editable-document.tsx +37 -36
- package/src/components/artifacts/editable-presentation.tsx +36 -18
- package/src/components/artifacts/editable-spreadsheet.tsx +5 -2
- package/src/components/artifacts/index.ts +44 -0
- package/src/components/artifacts/inline-html-document.ts +24 -0
- package/src/components/artifacts/latest-task-runner.ts +49 -0
- package/src/components/artifacts/presentation-editor.tsx +323 -67
- package/src/components/artifacts/published-html-artifact-frame.tsx +338 -5
- package/src/components/artifacts/session-artifact-viewer.tsx +376 -0
- package/src/components/artifacts/site-view.tsx +116 -0
- package/src/components/artifacts/spreadsheet-canvas.ts +5 -10
- package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
- package/src/components/artifacts/spreadsheet-grid.tsx +12 -13
- package/src/components/artifacts/visualization-assets/base.css +1015 -0
- package/src/components/artifacts/visualization-assets/helpers.html +349 -0
- package/src/components/artifacts/visualization-assets.gen.ts +3 -0
- package/src/components/browser-select-control.tsx +312 -0
- package/src/components/browser-viewer.tsx +1054 -314
- package/src/components/chat-composer.tsx +131 -85
- package/src/components/child-session-link.tsx +27 -0
- package/src/components/claude-mark.tsx +10 -0
- package/src/components/code-editor.tsx +13 -16
- package/src/components/composer-transcription-control.tsx +7 -5
- package/src/components/composer.tsx +376 -49
- package/src/components/computer-viewer.tsx +1041 -329
- package/src/components/copy-button.tsx +13 -5
- package/src/components/desktop-viewer.tsx +1 -1
- package/src/components/enrollment-consent.tsx +65 -7
- package/src/components/enrollment-device-flow.tsx +1 -1
- package/src/components/file-browser.tsx +158 -34
- package/src/components/fleet-tile.tsx +8 -16
- package/src/components/generated-video-player.tsx +4 -2
- package/src/components/grok-mark.tsx +33 -0
- package/src/components/human-input-form.tsx +455 -114
- package/src/components/human-input-surface.tsx +18 -7
- package/src/components/interaction-intervention-banner.tsx +2 -2
- package/src/components/machine-card.tsx +119 -4
- package/src/components/machine-dock-bar.tsx +1 -1
- package/src/components/machine-input-display.ts +23 -2
- package/src/components/machines/machine-detail.tsx +52 -2
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +8 -17
- package/src/components/markdown-table-layout.ts +132 -0
- package/src/components/markdown.tsx +473 -44
- package/src/components/message-timeline.tsx +2498 -663
- package/src/components/model-mark.tsx +90 -0
- package/src/components/model-picker.tsx +15 -4
- package/src/components/model-policy-picker-menu.tsx +424 -0
- package/src/components/model-policy-picker.tsx +337 -381
- package/src/components/open-geni-chat.tsx +319 -0
- package/src/components/open-geni-links.tsx +179 -0
- package/src/components/openrouter-mark.tsx +13 -0
- package/src/components/pierre-diff.tsx +41 -16
- package/src/components/pierre-file.tsx +36 -11
- package/src/components/preview-loading.tsx +154 -0
- package/src/components/queue-item-content.ts +14 -0
- package/src/components/queue-surface-implementation.tsx +132 -42
- package/src/components/queue-surface-state.tsx +3 -1
- package/src/components/sandbox-files.tsx +267 -35
- package/src/components/sandbox-terminal.tsx +48 -11
- package/src/components/sandbox-workspace.tsx +337 -135
- package/src/components/session-capability-frame.tsx +192 -0
- package/src/components/session-chrome.tsx +1027 -328
- package/src/components/session-commands-panel.tsx +126 -0
- package/src/components/session-commands.tsx +18 -0
- package/src/components/session-connection-request.tsx +97 -0
- package/src/components/session-conversation.tsx +512 -0
- package/src/components/session-list.tsx +250 -0
- package/src/components/session-mcp-capability-card.tsx +680 -0
- package/src/components/session-status.tsx +1 -1
- package/src/components/timeline-anchor.tsx +119 -0
- package/src/components/timeline-annotation-chrome.tsx +196 -0
- package/src/components/timeline-annotation-layout.ts +268 -0
- package/src/components/timeline-annotation-markers.tsx +185 -0
- package/src/components/timeline-annotation-reveal-context.ts +9 -0
- package/src/components/timeline-annotation-selection.tsx +106 -54
- package/src/components/timeline-annotation-shared.ts +245 -0
- package/src/components/timeline-annotations-dialog.tsx +185 -128
- package/src/components/timeline-annotations.tsx +189 -45
- package/src/components/timeline-reading-progress.ts +80 -0
- package/src/components/timeline-search.tsx +168 -0
- package/src/components/timeline-settlement.ts +60 -0
- package/src/components/tip-follow.ts +70 -7
- package/src/components/tooltip.tsx +38 -5
- package/src/components/usage-limit-notice.tsx +179 -0
- package/src/components/usage-member-list.tsx +792 -0
- package/src/components/usage-meter.tsx +382 -0
- package/src/components/use-viewer-menu-dismiss.ts +33 -0
- package/src/components/user-message-body.tsx +194 -49
- package/src/components/workspace-dock.tsx +141 -175
- package/src/composer.ts +6 -1
- package/src/connect-account-identity.tsx +34 -0
- package/src/connect-accounts.tsx +233 -0
- package/src/connect-chooser.tsx +336 -0
- package/src/connect-panel.tsx +99 -0
- package/src/connect-setup.tsx +307 -0
- package/src/connect.ts +60 -0
- package/src/connection-assets.d.ts +4 -0
- package/src/connection-catalog.tsx +162 -0
- package/src/connection-discovery.tsx +235 -0
- package/src/connection-installed.tsx +51 -0
- package/src/connection-logo.tsx +37 -0
- package/src/connection-service-logo.tsx +55 -0
- package/src/connection-service-presentation.ts +46 -0
- package/src/connection-skeleton.tsx +16 -0
- package/src/connection-type-picker.tsx +36 -0
- package/src/conversation-timeline.ts +92 -0
- package/src/desktop.ts +24 -0
- package/src/device-authorization.tsx +98 -0
- package/src/diffs.ts +16 -0
- package/src/discovery-cache.ts +42 -0
- package/src/editor.ts +13 -0
- package/src/embedded-session-client.ts +104 -0
- package/src/file-node-visibility.ts +66 -0
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/internal.ts +3 -2
- package/src/hooks/latest-question.ts +164 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-available-models.ts +20 -14
- package/src/hooks/use-browser-frame-stream.ts +25 -3
- package/src/hooks/use-browser-session.ts +271 -71
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-codex-accounts.ts +78 -21
- package/src/hooks/use-composer.ts +1034 -320
- package/src/hooks/use-computer-frame-stream.ts +39 -16
- package/src/hooks/use-computer-input-posture.ts +94 -0
- package/src/hooks/use-computer-session.ts +110 -32
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-connect.ts +23 -0
- package/src/hooks/use-desktop-stream.ts +19 -28
- package/src/hooks/use-file-attachments.ts +118 -10
- package/src/hooks/use-goal.ts +36 -9
- package/src/hooks/use-human-input.ts +31 -4
- package/src/hooks/use-last-started-turn-policy.ts +15 -13
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +253 -150
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +67 -0
- package/src/hooks/use-session-capabilities.ts +21 -2
- package/src/hooks/use-session-events.ts +759 -433
- package/src/hooks/use-session-lineage.ts +28 -6
- package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
- package/src/hooks/use-session.ts +75 -16
- package/src/hooks/use-slash-commands.ts +6 -11
- package/src/hooks/use-turn-queue.ts +113 -13
- package/src/hooks/use-usage.ts +100 -0
- package/src/hooks/use-voice-input.ts +12 -1
- package/src/hooks/use-workspace-capture.ts +3 -2
- package/src/hooks/use-workspace-edit.ts +23 -5
- package/src/hooks/use-workspace-sessions.ts +99 -11
- package/src/human-input.ts +12 -1
- package/src/identity-link-accounts.tsx +148 -0
- package/src/identity-link-consent.tsx +192 -0
- package/src/index.ts +157 -12
- package/src/interaction.ts +1 -0
- package/src/lib/error-message.ts +37 -0
- package/src/lib/format.ts +150 -14
- package/src/lib/host-theme.ts +511 -0
- package/src/lib/interaction-errors.ts +33 -0
- package/src/lib/menu-styles.ts +44 -0
- package/src/lib/pierre-diffs-loader.ts +53 -0
- package/src/lib/workbench-peers.ts +62 -0
- package/src/lib/xterm-theme.ts +23 -21
- package/src/machines.ts +9 -1
- package/src/mcp-connection-status.ts +35 -0
- package/src/model-policy.ts +223 -69
- package/src/older-history.ts +87 -0
- package/src/plugin-details.tsx +185 -0
- package/src/plugin-discovery.tsx +196 -0
- package/src/provider.tsx +55 -14
- package/src/realtime/dropdown-menu.tsx +19 -12
- package/src/realtime/realtime-control.tsx +274 -60
- package/src/session-auth-recommendation.ts +48 -0
- package/src/session-capability-policy.ts +83 -0
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +66 -2
- package/src/session.ts +18 -1
- package/src/sites-ui.tsx +402 -0
- package/src/sites.ts +3 -0
- package/src/skill-discovery.tsx +208 -0
- package/src/terminal.ts +26 -0
- package/src/timeline/activity-rail.tsx +171 -80
- package/src/timeline/allowance-exhausted-row.tsx +86 -0
- package/src/timeline/compaction-copy.ts +42 -0
- package/src/timeline/entrance.tsx +28 -3
- package/src/timeline/fleet-decision-projection.ts +2 -1
- package/src/timeline/fleet-decision-row.tsx +1 -0
- package/src/timeline/genie-loading.tsx +136 -0
- package/src/timeline/index.ts +11 -0
- package/src/timeline/knowledge-receipt.tsx +100 -0
- package/src/timeline/parsers.ts +2 -2
- package/src/timeline/platform-activity-row.tsx +216 -0
- package/src/timeline/presented-image.ts +49 -0
- package/src/timeline/projection.ts +1600 -84
- package/src/timeline/registry.ts +15 -3
- package/src/timeline/retained-image.ts +97 -0
- package/src/timeline/rolling-activity.tsx +92 -0
- package/src/timeline/screenshot-lightbox.tsx +115 -17
- package/src/timeline/shared.tsx +47 -15
- package/src/timeline/startup-preference.ts +33 -0
- package/src/timeline/startup-timings.tsx +145 -0
- package/src/timeline/tool-display-name.ts +8 -6
- package/src/timeline/tool-renderers.tsx +497 -128
- package/src/timeline/turn-summary.tsx +208 -44
- package/src/timeline/types.ts +186 -7
- package/src/types/machines.ts +10 -0
- package/src/usage/allowance-copy.ts +64 -0
- package/src/usage/summary.ts +93 -0
- package/src/usage.ts +50 -0
- package/styles/compiled.css +2098 -757
- package/styles/connect.css +284 -0
- package/styles/connect.d.ts +1 -0
- package/styles/effective-tokens.css +1 -0
- package/styles/index.css +201 -9
- package/styles/responsive.css +27 -1
- package/styles/session-capability.css +44 -0
- package/styles/tokens.css +135 -65
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-7WCLAEFV.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-FK6VG4LL.js +0 -90
- package/dist/chunk-FK6VG4LL.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-HUIKIDM5.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-KCGXPP5V.js.map +0 -1
- package/dist/chunk-KZ73RL76.js.map +0 -1
- package/dist/chunk-LAKLF4PA.js +0 -141
- package/dist/chunk-LAKLF4PA.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js +0 -465
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-PEI6RANI.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-WSK7G5LE.js +0 -170
- package/dist/chunk-WSK7G5LE.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js +0 -218
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js +0 -8810
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
- package/dist/hooks/use-packs.d.ts +0 -32
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
- package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
- package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
- package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
- package/src/hooks/use-packs.ts +0 -157
- /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GTR5XQ7X.js.map} +0 -0
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
sessionEventStreamCoveredThrough,
|
|
3
|
+
type SessionEvent,
|
|
4
|
+
type SessionStatus,
|
|
5
|
+
type StreamConnectionState,
|
|
6
|
+
} from "@opengeni/sdk";
|
|
7
|
+
import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
8
|
import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
|
|
9
|
+
import { createOlderHistoryLoadReceipt, type OlderHistoryLoadReceipt } from "../older-history";
|
|
4
10
|
import { buildTimeline, groupTimeline, sessionStatusFromEvents } from "../timeline/projection";
|
|
5
11
|
import type { TimelineItem } from "../timeline/types";
|
|
6
12
|
import type { EmbeddedSessionClientLike } from "../client";
|
|
13
|
+
import { normalizeError } from "../lib/error-message";
|
|
7
14
|
import { usePageLiveActivity } from "./internal";
|
|
15
|
+
import type { LatestQuestionOptions } from "./latest-question";
|
|
8
16
|
|
|
9
17
|
export type SessionEventsConnectionState = StreamConnectionState | "idle" | "ended" | "error";
|
|
10
18
|
|
|
@@ -20,10 +28,12 @@ export type UseSessionEventsOptions = EmbeddedSessionClientOverride & {
|
|
|
20
28
|
export type UseSessionEventsResult = {
|
|
21
29
|
/** Replayed + live events, ordered by sequence, no gaps, no duplicates. */
|
|
22
30
|
events: SessionEvent[];
|
|
23
|
-
/**
|
|
31
|
+
/** Renderable timeline, including the bounded navigation witness for a distant queued prompt. */
|
|
24
32
|
timeline: TimelineItem[];
|
|
25
33
|
/** Latest session status observed in the event log, if any. */
|
|
26
34
|
sessionStatus: SessionStatus | null;
|
|
35
|
+
/** Sequence of the retained status projection, including events evicted from the window. */
|
|
36
|
+
sessionStatusSequence?: number;
|
|
27
37
|
connectionState: SessionEventsConnectionState;
|
|
28
38
|
/** Highest sequence seen so far (0 before the first event). */
|
|
29
39
|
lastSequence: number;
|
|
@@ -33,12 +43,16 @@ export type UseSessionEventsResult = {
|
|
|
33
43
|
windowTruncated: boolean;
|
|
34
44
|
/** True until the initial tail window has been applied (windowed mode). */
|
|
35
45
|
initialLoading: boolean;
|
|
46
|
+
/** A history window (including an empty tail) succeeded for this session/replay
|
|
47
|
+
* identity. Stays true through later stream errors and navigation/reloads.
|
|
48
|
+
* Full replay has no snapshot-completion watermark and does not set this by itself. */
|
|
49
|
+
initialHistoryReady: boolean;
|
|
36
50
|
/** Whether older durable events are available before the current window. */
|
|
37
51
|
hasOlder: boolean;
|
|
38
52
|
/** True while an older window is being fetched. */
|
|
39
53
|
loadingOlder: boolean;
|
|
40
54
|
/** Prepend an older density-bounded window; resolves true when more remain. */
|
|
41
|
-
loadOlder: () =>
|
|
55
|
+
loadOlder: () => OlderHistoryLoadReceipt;
|
|
42
56
|
/**
|
|
43
57
|
* Durable events exist after the current window (history view jumped away
|
|
44
58
|
* from the live tip, or a forward page is incomplete).
|
|
@@ -65,13 +79,20 @@ export type UseSessionEventsResult = {
|
|
|
65
79
|
* "Jump to latest" when the tip is not in memory (history view).
|
|
66
80
|
*/
|
|
67
81
|
jumpToLatest: () => Promise<void>;
|
|
82
|
+
/** Resolve the newest eligible durable human question. Pending prompts use the
|
|
83
|
+
* optional queue destination and return null; started prompts load their actual
|
|
84
|
+
* turn context (pass `timeline` to MessageTimeline as `items`). */
|
|
85
|
+
jumpToLatestQuestion: (options?: LatestQuestionOptions) => Promise<number | null>;
|
|
86
|
+
/** Replace history with a bounded window containing this exact durable event. */
|
|
87
|
+
jumpToSequence: (sequence: number, options?: { signal?: AbortSignal }) => Promise<boolean>;
|
|
88
|
+
loadingTarget: boolean;
|
|
68
89
|
error: Error | null;
|
|
69
90
|
};
|
|
70
91
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
92
|
+
// Keep every browser history read inside one database batch, including the
|
|
93
|
+
// server's one-row continuation lookahead. A large total session must never
|
|
94
|
+
// turn one lazy page into dozens of sequential database round trips.
|
|
95
|
+
const SESSION_HISTORY_PAGE_SIZE = 1000;
|
|
75
96
|
const INITIAL_FETCH_CAP = 1;
|
|
76
97
|
const OLDER_GROUP_TARGET = 32;
|
|
77
98
|
const OLDER_FETCH_CAP = 2;
|
|
@@ -79,37 +100,26 @@ const NEWER_GROUP_TARGET = 32;
|
|
|
79
100
|
const NEWER_FETCH_CAP = 2;
|
|
80
101
|
const OLDEST_GROUP_TARGET = 32;
|
|
81
102
|
const OLDEST_FETCH_CAP = 2;
|
|
82
|
-
|
|
103
|
+
// A tail page may land inside one unusually dense turn. Permit exactly one
|
|
104
|
+
// additional bounded page to find its user/session boundary without turning a
|
|
105
|
+
// fresh open into an unbounded history walk.
|
|
106
|
+
const BOUNDARY_PAGE_CAP = 1;
|
|
107
|
+
// Foreground reconciliation is intentionally semantic, not merely time-based:
|
|
108
|
+
// tiny raw gaps can stay on SSE; medium raw gaps get one compact probe so a
|
|
109
|
+
// token-heavy single answer is not mistaken for hundreds of visible messages;
|
|
110
|
+
// only a large/complex missed window reloads the latest tail.
|
|
111
|
+
const FOREGROUND_DIRECT_REPLAY_MAX_SEQUENCES = 16;
|
|
112
|
+
const FOREGROUND_COMPACT_PROBE_MAX_SEQUENCES = SESSION_HISTORY_PAGE_SIZE;
|
|
113
|
+
const FOREGROUND_COMPACT_CATCHUP_MAX_EVENTS = 128;
|
|
114
|
+
const FOREGROUND_COMPACT_CATCHUP_MAX_GROUPS = 16;
|
|
115
|
+
const FOREGROUND_COMPACT_CATCHUP_MAX_BYTES = 512 * 1024;
|
|
83
116
|
const EMPTY_EVENTS: SessionEvent[] = [];
|
|
84
117
|
const encoder = new TextEncoder();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES = 4 * 1024;
|
|
91
|
-
const BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES = 48 * 1024;
|
|
92
|
-
const BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS = [
|
|
93
|
-
"id",
|
|
94
|
-
"callId",
|
|
95
|
-
"call_id",
|
|
96
|
-
"name",
|
|
97
|
-
"toolName",
|
|
98
|
-
"status",
|
|
99
|
-
"code",
|
|
100
|
-
"isError",
|
|
101
|
-
"stream",
|
|
102
|
-
"commandId",
|
|
103
|
-
"sequence",
|
|
104
|
-
"coalescedUntil",
|
|
105
|
-
"coalescedCount",
|
|
106
|
-
"firstSequence",
|
|
107
|
-
"lastSequence",
|
|
108
|
-
] as const;
|
|
109
|
-
|
|
110
|
-
export const SESSION_EVENT_BROWSER_MAX_BYTES = 8 * 1024 * 1024;
|
|
111
|
-
export const SESSION_EVENT_BROWSER_MAX_COUNT = 10_000;
|
|
112
|
-
export const SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES = 96 * 1024;
|
|
118
|
+
// Durable history remains on the server and is available through bounded
|
|
119
|
+
// navigation. Keep the live browser working set small across multiple tabs;
|
|
120
|
+
// serialized payloads also expand into objects and timeline projections.
|
|
121
|
+
export const SESSION_EVENT_BROWSER_MAX_BYTES = 16 * 1024 * 1024;
|
|
122
|
+
export const SESSION_EVENT_BROWSER_MAX_COUNT = 20_000;
|
|
113
123
|
export const SESSION_EVENT_BROWSER_PENDING_MAX_BYTES = 1024 * 1024;
|
|
114
124
|
export const SESSION_EVENT_BROWSER_PENDING_MAX_COUNT = 256;
|
|
115
125
|
|
|
@@ -142,20 +152,47 @@ export function useSessionEvents(
|
|
|
142
152
|
const replay = options.replay ?? "windowed";
|
|
143
153
|
const fullReplay = replay === "full" || after !== 0;
|
|
144
154
|
const streamKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${after}\u0000${fullReplay ? "full" : "windowed"}`;
|
|
155
|
+
// Bind callbacks to this read lifetime, including a return to a previous
|
|
156
|
+
// identity. Publish during render so old callbacks are fenced before passive
|
|
157
|
+
// cleanup can advance the navigation generation.
|
|
158
|
+
const navigationIdentityRef = useRef({ client, streamKey, enabled });
|
|
159
|
+
if (
|
|
160
|
+
navigationIdentityRef.current.client !== client ||
|
|
161
|
+
navigationIdentityRef.current.streamKey !== streamKey ||
|
|
162
|
+
navigationIdentityRef.current.enabled !== enabled
|
|
163
|
+
) {
|
|
164
|
+
navigationIdentityRef.current = { client, streamKey, enabled };
|
|
165
|
+
}
|
|
166
|
+
const navigationIdentity = navigationIdentityRef.current;
|
|
167
|
+
const ownsNavigation = useCallback(
|
|
168
|
+
() => navigationIdentityRef.current === navigationIdentity,
|
|
169
|
+
[navigationIdentity],
|
|
170
|
+
);
|
|
145
171
|
|
|
146
172
|
const [eventWindow, setEventWindow] = useState<BrowserSessionEventWindow>(EMPTY_EVENT_WINDOW);
|
|
173
|
+
// One navigation witness, not a second history cache. Raw events stay contiguous.
|
|
174
|
+
const [questionEvidence, setQuestionEvidence] = useState<{
|
|
175
|
+
client: EmbeddedSessionClientLike;
|
|
176
|
+
streamKey: string;
|
|
177
|
+
anchor: number;
|
|
178
|
+
events: SessionEvent[];
|
|
179
|
+
} | null>(null);
|
|
147
180
|
const [connectionState, setConnectionState] = useState<SessionEventsConnectionState>("idle");
|
|
148
181
|
const [error, setError] = useState<Error | null>(null);
|
|
182
|
+
const [newerError, setNewerError] = useState<Error | null>(null);
|
|
149
183
|
const [hasOlder, setHasOlder] = useState(false);
|
|
150
184
|
const [hasNewer, setHasNewer] = useState(false);
|
|
151
185
|
const [sessionStatusProjection, setSessionStatusProjection] = useState<SessionStatus | null>(
|
|
152
186
|
null,
|
|
153
187
|
);
|
|
154
188
|
const [initialLoading, setInitialLoading] = useState(true);
|
|
189
|
+
const [initialHistoryReady, setInitialHistoryReady] = useState(false);
|
|
155
190
|
const [loadingOlder, setLoadingOlder] = useState(false);
|
|
156
191
|
const [loadingNewer, setLoadingNewer] = useState(false);
|
|
157
192
|
const [loadingOldest, setLoadingOldest] = useState(false);
|
|
158
193
|
const [loadingLatest, setLoadingLatest] = useState(false);
|
|
194
|
+
const [loadingTarget, setLoadingTarget] = useState(false);
|
|
195
|
+
const loadingTargetRef = useRef(false);
|
|
159
196
|
const [streamEpoch, setStreamEpoch] = useState(0);
|
|
160
197
|
// "history" = reading a non-tip window; live SSE must not fill the gap.
|
|
161
198
|
const [viewMode, setViewMode] = useState<"live" | "history">("live");
|
|
@@ -171,9 +208,11 @@ export function useSessionEvents(
|
|
|
171
208
|
const loadingLatestRef = useRef(false);
|
|
172
209
|
const viewModeRef = useRef<"live" | "history">("live");
|
|
173
210
|
const initialWindowLoadedRef = useRef(false);
|
|
211
|
+
const reconcileAfterPageResumeRef = useRef(false);
|
|
174
212
|
const streamAbortRef = useRef<AbortController | null>(null);
|
|
175
213
|
const streamKeyRef = useRef<string | null>(null);
|
|
176
214
|
const generationRef = useRef(0);
|
|
215
|
+
const navigationGenerationRef = useRef(0);
|
|
177
216
|
const eventWindowRef = useRef<BrowserSessionEventWindow>(EMPTY_EVENT_WINDOW);
|
|
178
217
|
const sessionStatusRef = useRef<{
|
|
179
218
|
sequence: number;
|
|
@@ -186,6 +225,25 @@ export function useSessionEvents(
|
|
|
186
225
|
// new stream identity cannot expose the previous session's event log.
|
|
187
226
|
const [stateStreamKey, setStateStreamKey] = useState(streamKey);
|
|
188
227
|
|
|
228
|
+
// Reopening SSE after a prepend must not cancel the next history page.
|
|
229
|
+
// Navigation belongs to the session/client lifetime, not the transport.
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
navigationGenerationRef.current += 1;
|
|
232
|
+
setNewerError(null);
|
|
233
|
+
loadingOlderRef.current = false;
|
|
234
|
+
loadingNewerRef.current = false;
|
|
235
|
+
loadingOldestRef.current = false;
|
|
236
|
+
loadingTargetRef.current = false;
|
|
237
|
+
setQuestionEvidence(null);
|
|
238
|
+
setLoadingTarget(false);
|
|
239
|
+
setLoadingOlder(false);
|
|
240
|
+
setLoadingNewer(false);
|
|
241
|
+
setLoadingOldest(false);
|
|
242
|
+
return () => {
|
|
243
|
+
navigationGenerationRef.current += 1;
|
|
244
|
+
};
|
|
245
|
+
}, [client, workspaceId, sessionId, after, enabled, fullReplay]);
|
|
246
|
+
|
|
189
247
|
useEffect(() => {
|
|
190
248
|
// Reset the accumulated log only when the stream identity changes —
|
|
191
249
|
// pausing via `enabled: false` keeps the timeline visible.
|
|
@@ -204,6 +262,7 @@ export function useSessionEvents(
|
|
|
204
262
|
setLoadingOldest(false);
|
|
205
263
|
setLoadingLatest(false);
|
|
206
264
|
setInitialLoading(true);
|
|
265
|
+
setInitialHistoryReady(false);
|
|
207
266
|
lastSequenceRef.current = after;
|
|
208
267
|
streamResumeSequenceRef.current = after;
|
|
209
268
|
oldestSequenceRef.current = null;
|
|
@@ -217,29 +276,50 @@ export function useSessionEvents(
|
|
|
217
276
|
viewModeRef.current = "live";
|
|
218
277
|
setViewMode("live");
|
|
219
278
|
initialWindowLoadedRef.current = false;
|
|
279
|
+
reconcileAfterPageResumeRef.current = false;
|
|
220
280
|
}
|
|
221
281
|
// AbortController is advisory: custom SDK clients and async iterators may
|
|
222
282
|
// ignore it and resolve/yield after cleanup. Fence every effect instance so
|
|
223
283
|
// only the newest dependency generation can mutate refs or React state.
|
|
224
284
|
const generation = generationRef.current + 1;
|
|
225
285
|
generationRef.current = generation;
|
|
226
|
-
if (loadingOlderRef.current) {
|
|
227
|
-
loadingOlderRef.current = false;
|
|
228
|
-
setLoadingOlder(false);
|
|
229
|
-
}
|
|
230
286
|
if (!sessionId || !streamEnabled) {
|
|
287
|
+
// A page that stayed hidden beyond the live-activity grace deliberately
|
|
288
|
+
// closed its SSE connection. Replaying from the old cursor on return can
|
|
289
|
+
// drip a large background backlog through React in many fast batches and
|
|
290
|
+
// leave the pinned camera catching up long after the user is foregrounded.
|
|
291
|
+
// Remember that suspension so the next live effect replaces the browser
|
|
292
|
+
// window with one compact durable tail instead.
|
|
293
|
+
if (
|
|
294
|
+
sessionId &&
|
|
295
|
+
enabled &&
|
|
296
|
+
!pageLive &&
|
|
297
|
+
!fullReplay &&
|
|
298
|
+
viewModeRef.current === "live" &&
|
|
299
|
+
initialWindowLoadedRef.current
|
|
300
|
+
) {
|
|
301
|
+
reconcileAfterPageResumeRef.current = true;
|
|
302
|
+
}
|
|
231
303
|
setConnectionState("idle");
|
|
232
304
|
return;
|
|
233
305
|
}
|
|
234
306
|
// History view owns a non-tip window; do not open SSE (it would replay the
|
|
235
307
|
// entire gap into the browser). jumpToLatest / catching loadNewer resume live.
|
|
236
308
|
if (viewMode === "history") {
|
|
309
|
+
reconcileAfterPageResumeRef.current = false;
|
|
237
310
|
setConnectionState("idle");
|
|
238
311
|
setInitialLoading(false);
|
|
239
312
|
return;
|
|
240
313
|
}
|
|
314
|
+
const reconcileForegroundResume = reconcileAfterPageResumeRef.current && !fullReplay;
|
|
315
|
+
reconcileAfterPageResumeRef.current = false;
|
|
241
316
|
const controller = new AbortController();
|
|
242
|
-
const isCurrent = () =>
|
|
317
|
+
const isCurrent = () =>
|
|
318
|
+
generationRef.current === generation &&
|
|
319
|
+
!controller.signal.aborted &&
|
|
320
|
+
navigationIdentityRef.current.client === client &&
|
|
321
|
+
navigationIdentityRef.current.streamKey === streamKey &&
|
|
322
|
+
navigationIdentityRef.current.enabled === enabled;
|
|
243
323
|
streamAbortRef.current = controller;
|
|
244
324
|
// Batch yielded events into one React update per flush window so a long
|
|
245
325
|
// replay (thousands of events) does not render per event. Project every
|
|
@@ -282,9 +362,12 @@ export function useSessionEvents(
|
|
|
282
362
|
batchResumeSequence,
|
|
283
363
|
);
|
|
284
364
|
}
|
|
285
|
-
observeSessionStatus(batch, sessionStatusRef
|
|
365
|
+
const status = observeSessionStatus(batch, sessionStatusRef);
|
|
366
|
+
if (status !== undefined) {
|
|
367
|
+
setSessionStatusProjection(status);
|
|
368
|
+
}
|
|
286
369
|
const current = eventWindowRef.current;
|
|
287
|
-
const next =
|
|
370
|
+
const next = appendBrowserSessionEventWindow(current, batch);
|
|
288
371
|
const retained = {
|
|
289
372
|
...next,
|
|
290
373
|
truncated: current.truncated || next.truncated,
|
|
@@ -292,7 +375,7 @@ export function useSessionEvents(
|
|
|
292
375
|
eventWindowRef.current = retained;
|
|
293
376
|
setEventWindow(retained);
|
|
294
377
|
oldestSequenceRef.current = retained.events[0]?.sequence ?? null;
|
|
295
|
-
newestSequenceRef.current = retained.events
|
|
378
|
+
newestSequenceRef.current = maxResumeSequenceOrNull(retained.events);
|
|
296
379
|
if (retained.truncated) {
|
|
297
380
|
hasOlderRef.current = true;
|
|
298
381
|
setHasOlder(true);
|
|
@@ -305,6 +388,56 @@ export function useSessionEvents(
|
|
|
305
388
|
|
|
306
389
|
void (async () => {
|
|
307
390
|
try {
|
|
391
|
+
if (reconcileForegroundResume) {
|
|
392
|
+
setConnectionState("connecting");
|
|
393
|
+
const cursor = streamResumeSequenceRef.current;
|
|
394
|
+
let plan: ForegroundCatchupPlan = { kind: "resume" };
|
|
395
|
+
try {
|
|
396
|
+
plan = await planForegroundCatchup(client, workspaceId, sessionId, cursor, {
|
|
397
|
+
signal: controller.signal,
|
|
398
|
+
});
|
|
399
|
+
} catch {
|
|
400
|
+
if (controller.signal.aborted) return;
|
|
401
|
+
// This read is an optimization gate, not a new availability
|
|
402
|
+
// dependency. If it fails, preserve the SDK's exact cursor replay
|
|
403
|
+
// rather than blanking a usable timeline or failing the stream.
|
|
404
|
+
plan = { kind: "resume" };
|
|
405
|
+
}
|
|
406
|
+
if (!isCurrent()) return;
|
|
407
|
+
if (plan.kind === "append") {
|
|
408
|
+
const current = eventWindowRef.current;
|
|
409
|
+
assertAppendOrder(current.events, plan.events);
|
|
410
|
+
const status = observeSessionStatus(plan.events, sessionStatusRef);
|
|
411
|
+
const next = appendBrowserSessionEventWindow(current, plan.events);
|
|
412
|
+
const retained = {
|
|
413
|
+
...next,
|
|
414
|
+
truncated: current.truncated || next.truncated,
|
|
415
|
+
};
|
|
416
|
+
eventWindowRef.current = retained;
|
|
417
|
+
oldestSequenceRef.current = retained.events[0]?.sequence ?? null;
|
|
418
|
+
newestSequenceRef.current = maxResumeSequenceOrNull(retained.events);
|
|
419
|
+
lastSequenceRef.current = Math.max(lastSequenceRef.current, plan.resumeSequence);
|
|
420
|
+
streamResumeSequenceRef.current = Math.max(
|
|
421
|
+
streamResumeSequenceRef.current,
|
|
422
|
+
plan.resumeSequence,
|
|
423
|
+
);
|
|
424
|
+
if (status !== undefined) {
|
|
425
|
+
setSessionStatusProjection(status);
|
|
426
|
+
}
|
|
427
|
+
setEventWindow(retained);
|
|
428
|
+
if (retained.truncated) {
|
|
429
|
+
hasOlderRef.current = true;
|
|
430
|
+
setHasOlder(true);
|
|
431
|
+
}
|
|
432
|
+
} else if (plan.kind === "reload") {
|
|
433
|
+
// A large/complex missed window would make the foreground timeline
|
|
434
|
+
// and pinned camera chase many rapid commits. Keep the last known
|
|
435
|
+
// complete window visible until the bounded latest replacement is
|
|
436
|
+
// ready, then install that replacement atomically below.
|
|
437
|
+
initialWindowLoadedRef.current = false;
|
|
438
|
+
setError(null);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
308
441
|
if (!fullReplay && !initialWindowLoadedRef.current) {
|
|
309
442
|
setConnectionState("connecting");
|
|
310
443
|
// First paint is ONE compact fetch — the newest window, revealed at
|
|
@@ -312,25 +445,42 @@ export function useSessionEvents(
|
|
|
312
445
|
// reader actually scrolls up (the sentinel drives loadOlder).
|
|
313
446
|
const window = await loadEventWindow(client, workspaceId, sessionId, {
|
|
314
447
|
before: Number.MAX_SAFE_INTEGER,
|
|
315
|
-
pageSize:
|
|
448
|
+
pageSize: SESSION_HISTORY_PAGE_SIZE,
|
|
316
449
|
targetGroups: Number.POSITIVE_INFINITY,
|
|
317
450
|
maxFetches: INITIAL_FETCH_CAP,
|
|
451
|
+
boundaryPageCap: BOUNDARY_PAGE_CAP,
|
|
318
452
|
signal: controller.signal,
|
|
319
453
|
});
|
|
320
454
|
if (!isCurrent()) {
|
|
321
455
|
return;
|
|
322
456
|
}
|
|
323
|
-
observeSessionStatus(window.events, sessionStatusRef
|
|
457
|
+
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
324
458
|
const retained = boundBrowserSessionEventWindow(window.events);
|
|
459
|
+
// A replacement tail retires requests against the discarded window.
|
|
460
|
+
// Ordinary SSE reconnects preserve navigation, but splicing an old
|
|
461
|
+
// page into this new tail could leave an inaccessible history gap.
|
|
462
|
+
navigationGenerationRef.current += 1;
|
|
463
|
+
loadingOlderRef.current = false;
|
|
464
|
+
loadingNewerRef.current = false;
|
|
465
|
+
loadingOldestRef.current = false;
|
|
466
|
+
setLoadingOlder(false);
|
|
467
|
+
setLoadingNewer(false);
|
|
468
|
+
setLoadingOldest(false);
|
|
325
469
|
eventWindowRef.current = retained;
|
|
326
|
-
setEventWindow(retained);
|
|
327
470
|
oldestSequenceRef.current = retained.events[0]?.sequence ?? window.oldestSequence;
|
|
328
|
-
newestSequenceRef.current =
|
|
471
|
+
newestSequenceRef.current =
|
|
472
|
+
maxResumeSequenceOrNull(retained.events) ?? window.newestSequence;
|
|
329
473
|
hasOlderRef.current = window.hasOlder || retained.truncated;
|
|
330
474
|
hasNewerRef.current = false;
|
|
331
475
|
lastSequenceRef.current = window.newestSequence;
|
|
332
476
|
streamResumeSequenceRef.current = window.newestSequence;
|
|
333
477
|
initialWindowLoadedRef.current = true;
|
|
478
|
+
setInitialHistoryReady(true);
|
|
479
|
+
setError(null);
|
|
480
|
+
if (status !== undefined) {
|
|
481
|
+
setSessionStatusProjection(status);
|
|
482
|
+
}
|
|
483
|
+
setEventWindow(retained);
|
|
334
484
|
setHasOlder(window.hasOlder || retained.truncated);
|
|
335
485
|
setHasNewer(false);
|
|
336
486
|
setInitialLoading(false);
|
|
@@ -349,7 +499,7 @@ export function useSessionEvents(
|
|
|
349
499
|
.then(() => reconcileSession(sessionId))
|
|
350
500
|
.catch((cause) => {
|
|
351
501
|
if (isCurrent()) {
|
|
352
|
-
setError(
|
|
502
|
+
setError(normalizeError(cause));
|
|
353
503
|
}
|
|
354
504
|
});
|
|
355
505
|
},
|
|
@@ -361,7 +511,7 @@ export function useSessionEvents(
|
|
|
361
511
|
});
|
|
362
512
|
for await (const event of stream) {
|
|
363
513
|
if (!isCurrent()) break;
|
|
364
|
-
const boundedEvent =
|
|
514
|
+
const boundedEvent = event;
|
|
365
515
|
const boundedEventBytes = browserJsonBytes(boundedEvent);
|
|
366
516
|
const separatorBytes = pending.length === 0 ? 0 : 1;
|
|
367
517
|
if (
|
|
@@ -390,8 +540,12 @@ export function useSessionEvents(
|
|
|
390
540
|
} catch (cause) {
|
|
391
541
|
if (isCurrent()) {
|
|
392
542
|
flush();
|
|
393
|
-
setError(
|
|
543
|
+
setError(normalizeError(cause));
|
|
394
544
|
setConnectionState("error");
|
|
545
|
+
// A failed first compact-tail request has no later success path in
|
|
546
|
+
// this effect instance. End the loading gate so hosts can render the
|
|
547
|
+
// retryable error instead of an infinite initial spinner.
|
|
548
|
+
setInitialLoading(false);
|
|
395
549
|
}
|
|
396
550
|
}
|
|
397
551
|
})();
|
|
@@ -413,6 +567,8 @@ export function useSessionEvents(
|
|
|
413
567
|
workspaceId,
|
|
414
568
|
sessionId,
|
|
415
569
|
after,
|
|
570
|
+
enabled,
|
|
571
|
+
pageLive,
|
|
416
572
|
streamEnabled,
|
|
417
573
|
fullReplay,
|
|
418
574
|
streamKey,
|
|
@@ -422,102 +578,157 @@ export function useSessionEvents(
|
|
|
422
578
|
]);
|
|
423
579
|
|
|
424
580
|
const navigationBusy = (): boolean =>
|
|
581
|
+
loadingTargetRef.current ||
|
|
425
582
|
loadingOlderRef.current ||
|
|
426
583
|
loadingNewerRef.current ||
|
|
427
584
|
loadingOldestRef.current ||
|
|
428
585
|
loadingLatestRef.current;
|
|
429
586
|
|
|
430
|
-
const loadOlder = useCallback(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
587
|
+
const loadOlder = useCallback(
|
|
588
|
+
(): OlderHistoryLoadReceipt =>
|
|
589
|
+
createOlderHistoryLoadReceipt(async (markCommitted, preserveTail, markTailPreserved) => {
|
|
590
|
+
if (!ownsNavigation() || !sessionId || navigationBusy() || !hasOlderRef.current) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
const before = oldestSequenceRef.current;
|
|
594
|
+
if (before === null) {
|
|
595
|
+
oldestSequenceRef.current = null;
|
|
596
|
+
hasOlderRef.current = false;
|
|
597
|
+
setHasOlder(false);
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
const generation = navigationGenerationRef.current;
|
|
601
|
+
const isCurrent = () => ownsNavigation() && navigationGenerationRef.current === generation;
|
|
602
|
+
loadingOlderRef.current = true;
|
|
603
|
+
setLoadingOlder(true);
|
|
604
|
+
let published = false;
|
|
605
|
+
try {
|
|
606
|
+
const window = await loadEventWindow(client, workspaceId, sessionId, {
|
|
607
|
+
before,
|
|
608
|
+
pageSize: SESSION_HISTORY_PAGE_SIZE,
|
|
609
|
+
targetGroups: OLDER_GROUP_TARGET,
|
|
610
|
+
maxFetches: OLDER_FETCH_CAP,
|
|
611
|
+
isCurrent,
|
|
612
|
+
});
|
|
613
|
+
if (!isCurrent()) {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
if (window.events.length === 0) {
|
|
617
|
+
oldestSequenceRef.current = null;
|
|
618
|
+
hasOlderRef.current = false;
|
|
619
|
+
setHasOlder(false);
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
const current = eventWindowRef.current;
|
|
623
|
+
assertPrependOrder(current.events, window.events);
|
|
624
|
+
// Freeze the live iterator before replacing its in-memory window. Rows
|
|
625
|
+
// pending in the aborted iterator were never cursor-committed and will
|
|
626
|
+
// be replayed from the retained high-water mark below.
|
|
627
|
+
const next = boundBrowserSessionEventWindow([...window.events, ...current.events], {
|
|
628
|
+
direction: "oldest",
|
|
629
|
+
});
|
|
630
|
+
if (preserveTail && maxResumeSequence(next.events) < maxResumeSequence(current.events)) {
|
|
631
|
+
// Automatic viewport filling must not navigate away from the
|
|
632
|
+
// reader's retained tail. Explicit history navigation may do so.
|
|
633
|
+
markTailPreserved();
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
streamAbortRef.current?.abort();
|
|
637
|
+
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
638
|
+
const retained = {
|
|
639
|
+
...next,
|
|
640
|
+
truncated: current.truncated || next.truncated,
|
|
641
|
+
};
|
|
642
|
+
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
643
|
+
if (retainedOldest === null || retainedOldest >= before) {
|
|
644
|
+
throw new Error("@opengeni/react: loadOlder made no durable sequence progress");
|
|
645
|
+
}
|
|
646
|
+
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
647
|
+
const previousNewest = maxResumeSequenceOrNull(current.events);
|
|
648
|
+
eventWindowRef.current = retained;
|
|
649
|
+
markCommitted();
|
|
650
|
+
oldestSequenceRef.current = retainedOldest;
|
|
651
|
+
newestSequenceRef.current = retainedNewest;
|
|
652
|
+
streamResumeSequenceRef.current = maxResumeSequence(retained.events);
|
|
653
|
+
// Oldest-directed eviction can discard newer in-memory rows. That fact
|
|
654
|
+
// keeps windowTruncated true, but it does not imply older durable rows
|
|
655
|
+
// exist; only the backward DB page can answer hasOlder truthfully.
|
|
656
|
+
const olderStillAvailable = window.hasOlder;
|
|
657
|
+
hasOlderRef.current = olderStillAvailable;
|
|
658
|
+
const evictedLiveTail =
|
|
659
|
+
previousNewest !== null && retainedNewest !== null && retainedNewest < previousNewest;
|
|
660
|
+
let newer: boolean | null = null;
|
|
661
|
+
let enterHistory = false;
|
|
662
|
+
let reconnectLive = false;
|
|
663
|
+
if (viewModeRef.current === "history" || evictedLiveTail) {
|
|
664
|
+
const highWater = lastSequenceRef.current;
|
|
665
|
+
newer = retainedNewest !== null && (retainedNewest < highWater || retained.truncated);
|
|
666
|
+
hasNewerRef.current = newer;
|
|
667
|
+
if (evictedLiveTail) {
|
|
668
|
+
// A full oldest-directed browser window cannot also retain the
|
|
669
|
+
// live suffix. Keep the newly loaded history and page forward;
|
|
670
|
+
// reconnecting SSE here immediately newest-bounds the window and
|
|
671
|
+
// evicts every row the reader just requested.
|
|
672
|
+
viewModeRef.current = "history";
|
|
673
|
+
enterHistory = true;
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
// The retained merge is still one contiguous live suffix. Restart
|
|
677
|
+
// SSE from its cursor so events queued during the fetch are replayed.
|
|
678
|
+
reconnectLive = true;
|
|
679
|
+
}
|
|
680
|
+
loadingOlderRef.current = false;
|
|
681
|
+
startTransition(() => {
|
|
682
|
+
if (status !== undefined) {
|
|
683
|
+
setSessionStatusProjection(status);
|
|
684
|
+
}
|
|
685
|
+
setEventWindow(retained);
|
|
686
|
+
setHasOlder(olderStillAvailable);
|
|
687
|
+
if (newer !== null) {
|
|
688
|
+
setHasNewer(newer);
|
|
689
|
+
}
|
|
690
|
+
if (enterHistory) {
|
|
691
|
+
setViewMode("history");
|
|
692
|
+
} else if (reconnectLive) {
|
|
693
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
694
|
+
}
|
|
695
|
+
setLoadingOlder(false);
|
|
696
|
+
});
|
|
697
|
+
published = true;
|
|
698
|
+
return olderStillAvailable;
|
|
699
|
+
} catch (reason) {
|
|
700
|
+
if (!isCurrent()) return false;
|
|
701
|
+
throw reason;
|
|
702
|
+
} finally {
|
|
703
|
+
if (!published && isCurrent()) {
|
|
704
|
+
loadingOlderRef.current = false;
|
|
705
|
+
setLoadingOlder(false);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}),
|
|
709
|
+
[client, workspaceId, sessionId, ownsNavigation],
|
|
710
|
+
);
|
|
505
711
|
|
|
506
712
|
const loadOldest = useCallback(async (): Promise<boolean> => {
|
|
507
|
-
if (!sessionId || navigationBusy() || !hasOlderRef.current) {
|
|
713
|
+
if (!ownsNavigation() || !sessionId || navigationBusy() || !hasOlderRef.current) {
|
|
508
714
|
return false;
|
|
509
715
|
}
|
|
510
|
-
|
|
716
|
+
// Explicit window replacement supersedes unresolved question destinations.
|
|
717
|
+
const generation = ++navigationGenerationRef.current;
|
|
718
|
+
const isCurrent = () => ownsNavigation() && navigationGenerationRef.current === generation;
|
|
719
|
+
setQuestionEvidence(null);
|
|
511
720
|
loadingOldestRef.current = true;
|
|
512
721
|
setLoadingOldest(true);
|
|
722
|
+
let published = false;
|
|
513
723
|
try {
|
|
514
724
|
const window = await loadForwardEventWindow(client, workspaceId, sessionId, {
|
|
515
725
|
after: 0,
|
|
516
|
-
pageSize:
|
|
726
|
+
pageSize: SESSION_HISTORY_PAGE_SIZE,
|
|
517
727
|
targetGroups: OLDEST_GROUP_TARGET,
|
|
518
728
|
maxFetches: OLDEST_FETCH_CAP,
|
|
729
|
+
isCurrent,
|
|
519
730
|
});
|
|
520
|
-
if (
|
|
731
|
+
if (!isCurrent()) {
|
|
521
732
|
return false;
|
|
522
733
|
}
|
|
523
734
|
if (window.events.length === 0) {
|
|
@@ -526,17 +737,15 @@ export function useSessionEvents(
|
|
|
526
737
|
return false;
|
|
527
738
|
}
|
|
528
739
|
streamAbortRef.current?.abort();
|
|
529
|
-
observeSessionStatus(window.events, sessionStatusRef
|
|
740
|
+
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
530
741
|
// Keep the oldest prefix — never the middle or tip.
|
|
531
742
|
const retained = boundBrowserSessionEventWindow(window.events, { direction: "oldest" });
|
|
532
743
|
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
533
|
-
const retainedNewest = retained.events
|
|
744
|
+
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
534
745
|
eventWindowRef.current = retained;
|
|
535
|
-
setEventWindow(retained);
|
|
536
746
|
oldestSequenceRef.current = retainedOldest;
|
|
537
747
|
newestSequenceRef.current = retainedNewest;
|
|
538
748
|
hasOlderRef.current = false;
|
|
539
|
-
setHasOlder(false);
|
|
540
749
|
const highWater = Math.max(lastSequenceRef.current, retainedNewest ?? 0);
|
|
541
750
|
lastSequenceRef.current = highWater;
|
|
542
751
|
const newer =
|
|
@@ -544,19 +753,37 @@ export function useSessionEvents(
|
|
|
544
753
|
retained.truncated ||
|
|
545
754
|
(retainedNewest !== null && retainedNewest < highWater);
|
|
546
755
|
hasNewerRef.current = newer;
|
|
547
|
-
setHasNewer(newer);
|
|
548
756
|
initialWindowLoadedRef.current = true;
|
|
757
|
+
setInitialHistoryReady(true);
|
|
758
|
+
setError(null);
|
|
549
759
|
viewModeRef.current = "history";
|
|
550
|
-
|
|
760
|
+
loadingOldestRef.current = false;
|
|
761
|
+
setNewerError(null);
|
|
762
|
+
startTransition(() => {
|
|
763
|
+
if (status !== undefined) {
|
|
764
|
+
setSessionStatusProjection(status);
|
|
765
|
+
}
|
|
766
|
+
setEventWindow(retained);
|
|
767
|
+
setHasOlder(false);
|
|
768
|
+
setHasNewer(newer);
|
|
769
|
+
setViewMode("history");
|
|
770
|
+
setLoadingOldest(false);
|
|
771
|
+
});
|
|
772
|
+
published = true;
|
|
551
773
|
return newer;
|
|
774
|
+
} catch (reason) {
|
|
775
|
+
if (!isCurrent()) return false;
|
|
776
|
+
throw reason;
|
|
552
777
|
} finally {
|
|
553
|
-
|
|
554
|
-
|
|
778
|
+
if (!published && isCurrent()) {
|
|
779
|
+
loadingOldestRef.current = false;
|
|
780
|
+
setLoadingOldest(false);
|
|
781
|
+
}
|
|
555
782
|
}
|
|
556
|
-
}, [client, workspaceId, sessionId]);
|
|
783
|
+
}, [client, workspaceId, sessionId, ownsNavigation]);
|
|
557
784
|
|
|
558
785
|
const loadNewer = useCallback(async (): Promise<boolean> => {
|
|
559
|
-
if (!sessionId || navigationBusy() || !hasNewerRef.current) {
|
|
786
|
+
if (!ownsNavigation() || !sessionId || navigationBusy() || !hasNewerRef.current) {
|
|
560
787
|
return false;
|
|
561
788
|
}
|
|
562
789
|
const afterSequence = newestSequenceRef.current;
|
|
@@ -565,20 +792,24 @@ export function useSessionEvents(
|
|
|
565
792
|
setHasNewer(false);
|
|
566
793
|
return false;
|
|
567
794
|
}
|
|
568
|
-
const generation =
|
|
795
|
+
const generation = navigationGenerationRef.current;
|
|
796
|
+
const isCurrent = () => ownsNavigation() && navigationGenerationRef.current === generation;
|
|
569
797
|
loadingNewerRef.current = true;
|
|
570
798
|
setLoadingNewer(true);
|
|
799
|
+
let published = false;
|
|
571
800
|
try {
|
|
572
801
|
const window = await loadForwardEventWindow(client, workspaceId, sessionId, {
|
|
573
802
|
after: afterSequence,
|
|
574
|
-
pageSize:
|
|
803
|
+
pageSize: SESSION_HISTORY_PAGE_SIZE,
|
|
575
804
|
targetGroups: NEWER_GROUP_TARGET,
|
|
576
805
|
maxFetches: NEWER_FETCH_CAP,
|
|
806
|
+
isCurrent,
|
|
577
807
|
});
|
|
578
|
-
if (
|
|
808
|
+
if (!isCurrent()) {
|
|
579
809
|
return false;
|
|
580
810
|
}
|
|
581
811
|
if (window.events.length === 0) {
|
|
812
|
+
setNewerError(null);
|
|
582
813
|
hasNewerRef.current = false;
|
|
583
814
|
setHasNewer(false);
|
|
584
815
|
// Caught up with durable history — resume the live tip stream.
|
|
@@ -590,23 +821,20 @@ export function useSessionEvents(
|
|
|
590
821
|
}
|
|
591
822
|
const current = eventWindowRef.current;
|
|
592
823
|
assertAppendOrder(current.events, window.events);
|
|
593
|
-
observeSessionStatus(window.events, sessionStatusRef
|
|
824
|
+
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
594
825
|
const previousOldest = current.events[0]?.sequence ?? null;
|
|
595
826
|
// Moving forward through history: keep the newest suffix of the merge.
|
|
596
|
-
const next =
|
|
597
|
-
direction: "newest",
|
|
598
|
-
});
|
|
827
|
+
const next = appendBrowserSessionEventWindow(current, window.events);
|
|
599
828
|
const retained = {
|
|
600
829
|
...next,
|
|
601
830
|
truncated: current.truncated || next.truncated || window.hasNewer,
|
|
602
831
|
};
|
|
603
832
|
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
604
|
-
const retainedNewest = retained.events
|
|
833
|
+
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
605
834
|
if (retainedNewest === null || retainedNewest <= afterSequence) {
|
|
606
835
|
throw new Error("@opengeni/react: loadNewer made no durable sequence progress");
|
|
607
836
|
}
|
|
608
837
|
eventWindowRef.current = retained;
|
|
609
|
-
setEventWindow(retained);
|
|
610
838
|
oldestSequenceRef.current = retainedOldest;
|
|
611
839
|
newestSequenceRef.current = retainedNewest;
|
|
612
840
|
// Evicting the start while paging forward re-arms hasOlder.
|
|
@@ -619,69 +847,261 @@ export function useSessionEvents(
|
|
|
619
847
|
retainedOldest > 1))
|
|
620
848
|
) {
|
|
621
849
|
hasOlderRef.current = true;
|
|
622
|
-
setHasOlder(true);
|
|
623
850
|
}
|
|
851
|
+
const rearmedOlder = hasOlderRef.current;
|
|
624
852
|
const highWater = Math.max(lastSequenceRef.current, retainedNewest);
|
|
625
853
|
lastSequenceRef.current = highWater;
|
|
626
854
|
const newer = window.hasNewer || retainedNewest < highWater;
|
|
627
855
|
hasNewerRef.current = newer;
|
|
628
|
-
|
|
856
|
+
let resumeLive = false;
|
|
629
857
|
if (!newer) {
|
|
630
858
|
streamResumeSequenceRef.current = retainedNewest;
|
|
631
859
|
viewModeRef.current = "live";
|
|
632
|
-
|
|
633
|
-
setStreamEpoch((epoch) => epoch + 1);
|
|
860
|
+
resumeLive = true;
|
|
634
861
|
}
|
|
862
|
+
loadingNewerRef.current = false;
|
|
863
|
+
setNewerError(null);
|
|
864
|
+
startTransition(() => {
|
|
865
|
+
if (status !== undefined) {
|
|
866
|
+
setSessionStatusProjection(status);
|
|
867
|
+
}
|
|
868
|
+
setEventWindow(retained);
|
|
869
|
+
setHasOlder(rearmedOlder);
|
|
870
|
+
setHasNewer(newer);
|
|
871
|
+
if (resumeLive) {
|
|
872
|
+
setViewMode("live");
|
|
873
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
874
|
+
}
|
|
875
|
+
setLoadingNewer(false);
|
|
876
|
+
});
|
|
877
|
+
published = true;
|
|
635
878
|
return newer;
|
|
879
|
+
} catch (reason) {
|
|
880
|
+
// A settled request from an old navigation lifetime is not a failure of
|
|
881
|
+
// the current session. Preserve current loading/error state as well.
|
|
882
|
+
if (!isCurrent()) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
setNewerError(normalizeError(reason));
|
|
886
|
+
// Keep authorization and integrity failures actionable for callers;
|
|
887
|
+
// timeline-owned invocations attach their own explicit recovery UI.
|
|
888
|
+
throw reason;
|
|
636
889
|
} finally {
|
|
637
|
-
|
|
638
|
-
|
|
890
|
+
if (!published && isCurrent()) {
|
|
891
|
+
loadingNewerRef.current = false;
|
|
892
|
+
setLoadingNewer(false);
|
|
893
|
+
}
|
|
639
894
|
}
|
|
640
|
-
}, [client, workspaceId, sessionId]);
|
|
895
|
+
}, [client, workspaceId, sessionId, ownsNavigation]);
|
|
896
|
+
|
|
897
|
+
const jumpToSequence = useCallback(
|
|
898
|
+
async (sequence: number, navigationOptions?: { signal?: AbortSignal }): Promise<boolean> => {
|
|
899
|
+
const signal = navigationOptions?.signal;
|
|
900
|
+
if (!ownsNavigation() || !sessionId || !Number.isSafeInteger(sequence) || sequence < 1)
|
|
901
|
+
return false;
|
|
902
|
+
// A navigation that was already cancelled (for example Find closed before
|
|
903
|
+
// the caller could react) must not disturb any current view state.
|
|
904
|
+
if (signal?.aborted) return false;
|
|
905
|
+
// Explicit targets supersede both other targets and adjacent-page requests.
|
|
906
|
+
const generation = ++navigationGenerationRef.current;
|
|
907
|
+
setQuestionEvidence(null);
|
|
908
|
+
const current = () => generation === navigationGenerationRef.current && ownsNavigation();
|
|
909
|
+
const previousMode = viewModeRef.current;
|
|
910
|
+
streamAbortRef.current?.abort();
|
|
911
|
+
generationRef.current += 1;
|
|
912
|
+
viewModeRef.current = "history";
|
|
913
|
+
setViewMode("history");
|
|
914
|
+
loadingOlderRef.current = loadingNewerRef.current = loadingOldestRef.current = false;
|
|
915
|
+
setLoadingOlder(false);
|
|
916
|
+
setLoadingNewer(false);
|
|
917
|
+
setLoadingOldest(false);
|
|
918
|
+
loadingTargetRef.current = true;
|
|
919
|
+
setLoadingTarget(true);
|
|
920
|
+
let published = false;
|
|
921
|
+
try {
|
|
922
|
+
const [before, following] = await Promise.all([
|
|
923
|
+
loadPreviousPage(client, workspaceId, sessionId, sequence + 1, { pageSize: 128 }),
|
|
924
|
+
loadNextPage(client, workspaceId, sessionId, sequence, { pageSize: 128 }),
|
|
925
|
+
]);
|
|
926
|
+
// Close-during-fetch fence: a pending jump must not replace the window
|
|
927
|
+
// after its owner (for example ConversationFind) has gone away. The
|
|
928
|
+
// finally block still settles loadingTarget for this generation.
|
|
929
|
+
if (signal?.aborted) return false;
|
|
930
|
+
if (!current()) return false;
|
|
931
|
+
if (!before.some((event) => event.sequence === sequence)) return false;
|
|
932
|
+
// Bound each side, keeping the target even if its message alone exceeds
|
|
933
|
+
// the ordinary byte budget. Never evict the very event being navigated to.
|
|
934
|
+
const prefix = boundBrowserSessionEventWindow(before, {
|
|
935
|
+
direction: "newest",
|
|
936
|
+
maxBytes: SESSION_EVENT_BROWSER_MAX_BYTES / 2,
|
|
937
|
+
});
|
|
938
|
+
const retained = boundBrowserSessionEventWindow([...prefix.events, ...following], {
|
|
939
|
+
direction: "oldest",
|
|
940
|
+
});
|
|
941
|
+
if (!retained.events.some((event) => event.sequence === sequence)) return false;
|
|
942
|
+
streamAbortRef.current?.abort();
|
|
943
|
+
generationRef.current += 1;
|
|
944
|
+
const status = observeSessionStatus(retained.events, sessionStatusRef);
|
|
945
|
+
eventWindowRef.current = retained;
|
|
946
|
+
oldestSequenceRef.current = retained.events[0]!.sequence;
|
|
947
|
+
newestSequenceRef.current = maxResumeSequenceOrNull(retained.events);
|
|
948
|
+
lastSequenceRef.current = Math.max(lastSequenceRef.current, newestSequenceRef.current ?? 0);
|
|
949
|
+
hasOlderRef.current = !isLogStart(retained.events[0]!);
|
|
950
|
+
// Compact/byte-bounded pages may be short despite more durable history.
|
|
951
|
+
// Only an empty forward page proves the end (same rule as loadNewer).
|
|
952
|
+
hasNewerRef.current =
|
|
953
|
+
following.length > 0 ||
|
|
954
|
+
retained.truncated ||
|
|
955
|
+
(newestSequenceRef.current ?? 0) < lastSequenceRef.current;
|
|
956
|
+
initialWindowLoadedRef.current = true;
|
|
957
|
+
setInitialHistoryReady(true);
|
|
958
|
+
setError(null);
|
|
959
|
+
viewModeRef.current = "history";
|
|
960
|
+
setEventWindow(retained);
|
|
961
|
+
setHasOlder(hasOlderRef.current);
|
|
962
|
+
setHasNewer(hasNewerRef.current);
|
|
963
|
+
setInitialLoading(false);
|
|
964
|
+
setViewMode("history");
|
|
965
|
+
setNewerError(null);
|
|
966
|
+
if (status !== undefined) setSessionStatusProjection(status);
|
|
967
|
+
published = true;
|
|
968
|
+
return true;
|
|
969
|
+
} catch (reason) {
|
|
970
|
+
if (!current() || signal?.aborted) return false;
|
|
971
|
+
throw reason;
|
|
972
|
+
} finally {
|
|
973
|
+
if (current()) {
|
|
974
|
+
loadingTargetRef.current = false;
|
|
975
|
+
setLoadingTarget(false);
|
|
976
|
+
if (!published && previousMode === "live") {
|
|
977
|
+
viewModeRef.current = "live";
|
|
978
|
+
setViewMode("live");
|
|
979
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
[client, workspaceId, sessionId, ownsNavigation],
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
const jumpToLatestQuestion = useCallback(
|
|
988
|
+
async (questionOptions?: LatestQuestionOptions): Promise<number | null> => {
|
|
989
|
+
if (!sessionId || !ownsNavigation()) return null;
|
|
990
|
+
let generation = navigationGenerationRef.current;
|
|
991
|
+
const current = () => ownsNavigation() && generation === navigationGenerationRef.current;
|
|
992
|
+
try {
|
|
993
|
+
// Optional action code stays off the session-open path. Capture identity
|
|
994
|
+
// before the import and fence its completion just like network reads.
|
|
995
|
+
const { resolveLatestQuestion } = await import("./latest-question");
|
|
996
|
+
if (!current()) return null;
|
|
997
|
+
const destination = await resolveLatestQuestion({
|
|
998
|
+
client,
|
|
999
|
+
workspaceId,
|
|
1000
|
+
sessionId,
|
|
1001
|
+
isCurrent: current,
|
|
1002
|
+
resumeSequence: maxResumeSequence,
|
|
1003
|
+
options: questionOptions,
|
|
1004
|
+
});
|
|
1005
|
+
if (!destination || !current()) return null;
|
|
1006
|
+
if (!eventWindowRef.current.events.some((event) => event.sequence === destination.anchor)) {
|
|
1007
|
+
const navigation = jumpToSequence(destination.anchor);
|
|
1008
|
+
generation = navigationGenerationRef.current;
|
|
1009
|
+
if (!(await navigation) || !current()) return null;
|
|
1010
|
+
}
|
|
1011
|
+
setQuestionEvidence({
|
|
1012
|
+
client,
|
|
1013
|
+
streamKey,
|
|
1014
|
+
anchor: destination.anchor,
|
|
1015
|
+
events: destination.events,
|
|
1016
|
+
});
|
|
1017
|
+
return destination.questionSequence;
|
|
1018
|
+
} catch (reason) {
|
|
1019
|
+
if (!current()) return null;
|
|
1020
|
+
throw reason;
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
[client, workspaceId, sessionId, streamKey, ownsNavigation, jumpToSequence],
|
|
1024
|
+
);
|
|
641
1025
|
|
|
642
1026
|
const jumpToLatest = useCallback(async (): Promise<void> => {
|
|
1027
|
+
// An old host retry closure must not clear a replacement session's error
|
|
1028
|
+
// or abort its live feed before the passive effect cleanup has run.
|
|
1029
|
+
if (!ownsNavigation()) return;
|
|
1030
|
+
if (loadingTargetRef.current) {
|
|
1031
|
+
navigationGenerationRef.current += 1;
|
|
1032
|
+
loadingTargetRef.current = false;
|
|
1033
|
+
setLoadingTarget(false);
|
|
1034
|
+
}
|
|
643
1035
|
if (!sessionId || navigationBusy()) {
|
|
644
1036
|
return;
|
|
645
1037
|
}
|
|
1038
|
+
navigationGenerationRef.current += 1;
|
|
1039
|
+
setQuestionEvidence(null);
|
|
646
1040
|
loadingLatestRef.current = true;
|
|
647
1041
|
setLoadingLatest(true);
|
|
1042
|
+
setNewerError(null);
|
|
1043
|
+
setError(null);
|
|
1044
|
+
let published = false;
|
|
648
1045
|
try {
|
|
649
1046
|
streamAbortRef.current?.abort();
|
|
650
1047
|
hasNewerRef.current = false;
|
|
651
|
-
setHasNewer(false);
|
|
652
1048
|
hasOlderRef.current = false;
|
|
653
|
-
setHasOlder(false);
|
|
654
1049
|
newestSequenceRef.current = null;
|
|
655
1050
|
oldestSequenceRef.current = null;
|
|
656
1051
|
eventWindowRef.current = EMPTY_EVENT_WINDOW;
|
|
657
|
-
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
658
1052
|
initialWindowLoadedRef.current = false;
|
|
659
1053
|
streamResumeSequenceRef.current = after;
|
|
660
|
-
setInitialLoading(true);
|
|
661
1054
|
viewModeRef.current = "live";
|
|
662
|
-
|
|
663
|
-
|
|
1055
|
+
loadingLatestRef.current = false;
|
|
1056
|
+
startTransition(() => {
|
|
1057
|
+
setHasNewer(false);
|
|
1058
|
+
setHasOlder(false);
|
|
1059
|
+
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
1060
|
+
setInitialLoading(true);
|
|
1061
|
+
setViewMode("live");
|
|
1062
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
1063
|
+
setLoadingLatest(false);
|
|
1064
|
+
});
|
|
1065
|
+
published = true;
|
|
664
1066
|
// Effect reloads the tip. The tip fetch itself is owned by the effect.
|
|
665
1067
|
await Promise.resolve();
|
|
666
1068
|
} finally {
|
|
667
|
-
|
|
668
|
-
|
|
1069
|
+
if (!published) {
|
|
1070
|
+
loadingLatestRef.current = false;
|
|
1071
|
+
setLoadingLatest(false);
|
|
1072
|
+
}
|
|
669
1073
|
}
|
|
670
|
-
}, [after, sessionId]);
|
|
1074
|
+
}, [after, sessionId, ownsNavigation]);
|
|
671
1075
|
|
|
672
1076
|
const identityMatches = stateStreamKey === streamKey;
|
|
673
1077
|
const visibleEvents = identityMatches ? eventWindow.events : EMPTY_EVENTS;
|
|
674
|
-
const timeline = useMemo(() =>
|
|
1078
|
+
const timeline = useMemo(() => {
|
|
1079
|
+
const witness =
|
|
1080
|
+
questionEvidence?.client === client &&
|
|
1081
|
+
questionEvidence.streamKey === streamKey &&
|
|
1082
|
+
visibleEvents.some((event) => event.sequence === questionEvidence.anchor)
|
|
1083
|
+
? questionEvidence.events
|
|
1084
|
+
: EMPTY_EVENTS;
|
|
1085
|
+
const timelineOptions = { partialStart: hasOlder || eventWindow.truncated || after > 0 };
|
|
1086
|
+
if (witness.length === 0) return buildTimeline(visibleEvents, timelineOptions);
|
|
1087
|
+
const ids = new Set(visibleEvents.map((event) => event.id));
|
|
1088
|
+
return buildTimeline(
|
|
1089
|
+
[...visibleEvents, ...witness.filter((event) => !ids.has(event.id))],
|
|
1090
|
+
timelineOptions,
|
|
1091
|
+
);
|
|
1092
|
+
}, [visibleEvents, hasOlder, eventWindow.truncated, after, questionEvidence, client, streamKey]);
|
|
675
1093
|
|
|
676
1094
|
return {
|
|
677
1095
|
events: visibleEvents,
|
|
678
1096
|
timeline,
|
|
679
1097
|
sessionStatus: identityMatches ? sessionStatusProjection : null,
|
|
1098
|
+
sessionStatusSequence: identityMatches ? sessionStatusRef.current.sequence : 0,
|
|
680
1099
|
connectionState: identityMatches ? connectionState : "idle",
|
|
681
1100
|
lastSequence: identityMatches ? lastSequenceRef.current : after,
|
|
682
1101
|
windowBytes: identityMatches ? eventWindow.bytes : 2,
|
|
683
1102
|
windowTruncated: identityMatches ? eventWindow.truncated : false,
|
|
684
1103
|
initialLoading: fullReplay ? false : identityMatches ? initialLoading : true,
|
|
1104
|
+
initialHistoryReady: identityMatches && initialHistoryReady,
|
|
685
1105
|
hasOlder: !identityMatches ? false : hasOlder,
|
|
686
1106
|
loadingOlder: !identityMatches ? false : loadingOlder,
|
|
687
1107
|
loadOlder,
|
|
@@ -692,7 +1112,10 @@ export function useSessionEvents(
|
|
|
692
1112
|
loadOldest,
|
|
693
1113
|
loadingLatest: !identityMatches ? false : loadingLatest,
|
|
694
1114
|
jumpToLatest,
|
|
695
|
-
|
|
1115
|
+
jumpToLatestQuestion,
|
|
1116
|
+
jumpToSequence,
|
|
1117
|
+
loadingTarget: identityMatches && loadingTarget,
|
|
1118
|
+
error: identityMatches ? (newerError ?? error) : null,
|
|
696
1119
|
};
|
|
697
1120
|
}
|
|
698
1121
|
|
|
@@ -701,9 +1124,10 @@ export function useSessionEvents(
|
|
|
701
1124
|
* accumulation retains the newest suffix; backward paging retains the oldest
|
|
702
1125
|
* prefix so newly fetched history cannot be immediately evicted. This is
|
|
703
1126
|
* deliberately separate from durable history and transport paging: when a
|
|
704
|
-
* backward page evicts the live tail, the hook
|
|
705
|
-
*
|
|
706
|
-
*
|
|
1127
|
+
* backward page evicts the live tail, the hook enters bounded history mode and
|
|
1128
|
+
* preserves the highest-ever-observed sequence separately for forward paging.
|
|
1129
|
+
* The source event remains durable in PostgreSQL throughout. An event larger
|
|
1130
|
+
* than the byte target is retained alone, never replaced by a lossy preview.
|
|
707
1131
|
*/
|
|
708
1132
|
export function boundBrowserSessionEventWindow(
|
|
709
1133
|
events: readonly SessionEvent[],
|
|
@@ -715,7 +1139,7 @@ export function boundBrowserSessionEventWindow(
|
|
|
715
1139
|
): BrowserSessionEventWindow {
|
|
716
1140
|
const maxBytes = Math.max(1024, options.maxBytes ?? SESSION_EVENT_BROWSER_MAX_BYTES);
|
|
717
1141
|
const maxCount = Math.max(1, Math.floor(options.maxCount ?? SESSION_EVENT_BROWSER_MAX_COUNT));
|
|
718
|
-
const safe = events
|
|
1142
|
+
const safe = events;
|
|
719
1143
|
const selected: SessionEvent[] = [];
|
|
720
1144
|
let bytes = 2; // []
|
|
721
1145
|
const direction = options.direction ?? "newest";
|
|
@@ -726,7 +1150,7 @@ export function boundBrowserSessionEventWindow(
|
|
|
726
1150
|
const event = safe[index]!;
|
|
727
1151
|
const eventBytes = browserJsonBytes(event);
|
|
728
1152
|
const separator = selected.length === 0 ? 0 : 1;
|
|
729
|
-
if (bytes + separator + eventBytes > maxBytes) break;
|
|
1153
|
+
if (selected.length > 0 && bytes + separator + eventBytes > maxBytes) break;
|
|
730
1154
|
selected.push(event);
|
|
731
1155
|
bytes += separator + eventBytes;
|
|
732
1156
|
}
|
|
@@ -738,255 +1162,41 @@ export function boundBrowserSessionEventWindow(
|
|
|
738
1162
|
};
|
|
739
1163
|
}
|
|
740
1164
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
!event.type.includes("\n") &&
|
|
754
|
-
!event.type.includes("\r");
|
|
755
|
-
const clientEventId = boundBrowserOptionalText(
|
|
756
|
-
event.clientEventId,
|
|
757
|
-
BROWSER_EVENT_CLIENT_ID_MAX_BYTES,
|
|
758
|
-
);
|
|
759
|
-
const duplicateReason = boundBrowserOptionalText(
|
|
760
|
-
event.duplicateReason,
|
|
761
|
-
BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES,
|
|
762
|
-
);
|
|
763
|
-
|
|
764
|
-
if (
|
|
765
|
-
serialized.serializable &&
|
|
766
|
-
originalBytes !== null &&
|
|
767
|
-
originalBytes <= SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES &&
|
|
768
|
-
typeIsSafe &&
|
|
769
|
-
clientEventId === event.clientEventId &&
|
|
770
|
-
duplicateReason === event.duplicateReason
|
|
771
|
-
) {
|
|
772
|
-
return event;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const envelopeProjection = [
|
|
776
|
-
!typeIsSafe
|
|
777
|
-
? browserEnvelopeFieldProjection("type", event.type, "session.event.envelope_omitted")
|
|
778
|
-
: null,
|
|
779
|
-
clientEventId !== event.clientEventId
|
|
780
|
-
? browserEnvelopeFieldProjection("clientEventId", event.clientEventId, clientEventId)
|
|
781
|
-
: null,
|
|
782
|
-
duplicateReason !== event.duplicateReason
|
|
783
|
-
? browserEnvelopeFieldProjection("duplicateReason", event.duplicateReason, duplicateReason)
|
|
784
|
-
: null,
|
|
785
|
-
].filter((field) => field !== null);
|
|
786
|
-
const payloadSerialization = browserSerialize(event.payload);
|
|
787
|
-
const payloadBytes = payloadSerialization.serializable
|
|
788
|
-
? encoder.encode(payloadSerialization.value).byteLength
|
|
789
|
-
: null;
|
|
790
|
-
const preview = truncateBrowserUtf8Middle(
|
|
791
|
-
payloadSerialization.value,
|
|
792
|
-
BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES,
|
|
793
|
-
);
|
|
794
|
-
const truncation = {
|
|
795
|
-
truncated: true as const,
|
|
796
|
-
surface: "browser_legacy_guard" as const,
|
|
797
|
-
reason: serialized.serializable ? "event_envelope_bytes_exceeded" : "event_not_serializable",
|
|
798
|
-
originalBytes,
|
|
799
|
-
deliveredBytes: 0,
|
|
800
|
-
omittedBytes: originalBytes,
|
|
801
|
-
estimatedOriginalTokens: originalBytes === null ? null : Math.ceil(originalBytes / 4),
|
|
802
|
-
estimatedDeliveredTokens: 0,
|
|
803
|
-
fullEvidence: { available: false as const, reason: "not_retained" as const },
|
|
804
|
-
details: [
|
|
805
|
-
{
|
|
806
|
-
path: "$.payload",
|
|
807
|
-
kind: "payload_preview",
|
|
808
|
-
originalBytes: payloadBytes,
|
|
809
|
-
deliveredBytes: browserUtf8Bytes(preview),
|
|
810
|
-
},
|
|
811
|
-
],
|
|
812
|
-
};
|
|
813
|
-
const payload: Record<string, unknown> = {
|
|
814
|
-
...browserPayloadIdentity(event.payload),
|
|
815
|
-
preview,
|
|
816
|
-
...(envelopeProjection.length > 0
|
|
817
|
-
? {
|
|
818
|
-
originalType: boundBrowserText(event.type, BROWSER_EVENT_TYPE_MAX_BYTES),
|
|
819
|
-
envelopeProjection: {
|
|
820
|
-
truncated: true,
|
|
821
|
-
surface: "browser_legacy_guard",
|
|
822
|
-
fields: envelopeProjection,
|
|
823
|
-
},
|
|
824
|
-
}
|
|
825
|
-
: {}),
|
|
826
|
-
truncation,
|
|
827
|
-
};
|
|
828
|
-
const bounded: SessionEvent = {
|
|
829
|
-
id: boundBrowserText(event.id, BROWSER_EVENT_ID_MAX_BYTES),
|
|
830
|
-
workspaceId: boundBrowserText(event.workspaceId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
831
|
-
sessionId: boundBrowserText(event.sessionId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
832
|
-
sequence: event.sequence,
|
|
833
|
-
type: typeIsSafe ? event.type : "session.event.envelope_omitted",
|
|
834
|
-
payload,
|
|
835
|
-
occurredAt: boundBrowserText(event.occurredAt, BROWSER_EVENT_ID_MAX_BYTES),
|
|
836
|
-
clientEventId,
|
|
837
|
-
turnId: boundBrowserOptionalText(event.turnId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
838
|
-
turnGeneration: event.turnGeneration,
|
|
839
|
-
turnAttemptId: boundBrowserOptionalText(event.turnAttemptId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
840
|
-
turnAssociation: event.turnAssociation,
|
|
841
|
-
duplicateOfEventId: boundBrowserOptionalText(
|
|
842
|
-
event.duplicateOfEventId,
|
|
843
|
-
BROWSER_EVENT_ID_MAX_BYTES,
|
|
844
|
-
),
|
|
845
|
-
duplicateReason,
|
|
846
|
-
};
|
|
847
|
-
|
|
848
|
-
settleBrowserEventTruncation(bounded, truncation);
|
|
849
|
-
if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
|
|
850
|
-
payload.preview = truncateBrowserUtf8Middle(String(payload.preview), 4 * 1024);
|
|
851
|
-
truncation.details = truncation.details.slice(0, 1);
|
|
852
|
-
settleBrowserEventTruncation(bounded, truncation);
|
|
853
|
-
}
|
|
854
|
-
if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
|
|
855
|
-
bounded.clientEventId = null;
|
|
856
|
-
bounded.duplicateReason = null;
|
|
857
|
-
payload.preview = "[legacy event omitted at the browser byte boundary]";
|
|
858
|
-
settleBrowserEventTruncation(bounded, truncation);
|
|
859
|
-
}
|
|
860
|
-
return bounded;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
function browserJsonBytes(value: unknown): number {
|
|
864
|
-
return encoder.encode(browserSerialize(value).value).byteLength;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
function browserSerialize(value: unknown): { value: string; serializable: boolean } {
|
|
868
|
-
try {
|
|
869
|
-
const serialized = JSON.stringify(value);
|
|
870
|
-
return { value: serialized === undefined ? "null" : serialized, serializable: true };
|
|
871
|
-
} catch {
|
|
872
|
-
return {
|
|
873
|
-
value: '"[unserializable event payload omitted at browser boundary]"',
|
|
874
|
-
serializable: false,
|
|
875
|
-
};
|
|
1165
|
+
/** @internal Append immutable events without serializing the retained history again. */
|
|
1166
|
+
export function appendBrowserSessionEventWindow(
|
|
1167
|
+
current: BrowserSessionEventWindow,
|
|
1168
|
+
batch: readonly SessionEvent[],
|
|
1169
|
+
options: { maxBytes?: number; maxCount?: number } = {},
|
|
1170
|
+
): BrowserSessionEventWindow {
|
|
1171
|
+
const maxBytes = Math.max(1024, options.maxBytes ?? SESSION_EVENT_BROWSER_MAX_BYTES);
|
|
1172
|
+
const maxCount = Math.max(1, Math.floor(options.maxCount ?? SESSION_EVENT_BROWSER_MAX_COUNT));
|
|
1173
|
+
const events = [...current.events, ...batch];
|
|
1174
|
+
let bytes = current.bytes;
|
|
1175
|
+
for (let index = 0; index < batch.length; index += 1) {
|
|
1176
|
+
bytes += browserJsonBytes(batch[index]) + (current.events.length + index > 0 ? 1 : 0);
|
|
876
1177
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
for (const field of BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS) {
|
|
884
|
-
const value = record[field];
|
|
885
|
-
if (typeof value === "string") {
|
|
886
|
-
identity[field] = boundBrowserText(value, BROWSER_EVENT_ID_MAX_BYTES);
|
|
887
|
-
} else if (typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
888
|
-
identity[field] = value;
|
|
889
|
-
}
|
|
1178
|
+
let start = 0;
|
|
1179
|
+
// Preserve the newest event even when it alone exceeds the byte target,
|
|
1180
|
+
// matching the full-window reducer and retaining exact cursor progress.
|
|
1181
|
+
while (events.length - start > 1 && (bytes > maxBytes || events.length - start > maxCount)) {
|
|
1182
|
+
bytes -= browserJsonBytes(events[start]) + 1;
|
|
1183
|
+
start += 1;
|
|
890
1184
|
}
|
|
891
|
-
return identity;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function browserEnvelopeFieldProjection(
|
|
895
|
-
field: string,
|
|
896
|
-
original: string | null | undefined,
|
|
897
|
-
delivered: string | null | undefined,
|
|
898
|
-
): { field: string; originalBytes: number; deliveredBytes: number } {
|
|
899
1185
|
return {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1186
|
+
events: start === 0 ? events : events.slice(start),
|
|
1187
|
+
bytes,
|
|
1188
|
+
truncated: current.truncated || start > 0,
|
|
903
1189
|
};
|
|
904
1190
|
}
|
|
905
1191
|
|
|
906
|
-
function
|
|
907
|
-
value
|
|
908
|
-
maxBytes: number,
|
|
909
|
-
): T {
|
|
910
|
-
return (typeof value === "string" ? boundBrowserText(value, maxBytes) : value) as T;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
function boundBrowserText(value: string, maxBytes: number): string {
|
|
914
|
-
const bytes = encoder.encode(value);
|
|
915
|
-
if (bytes.byteLength <= maxBytes) return value;
|
|
916
|
-
const marker = "…[truncated]";
|
|
917
|
-
const prefixBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
|
|
918
|
-
let prefixEnd = Math.min(prefixBudget, bytes.byteLength);
|
|
919
|
-
while (
|
|
920
|
-
prefixEnd > 0 &&
|
|
921
|
-
prefixEnd < bytes.byteLength &&
|
|
922
|
-
isBrowserUtf8Continuation(bytes[prefixEnd]!)
|
|
923
|
-
) {
|
|
924
|
-
prefixEnd -= 1;
|
|
925
|
-
}
|
|
926
|
-
return `${decoder.decode(bytes.subarray(0, prefixEnd))}${marker}`;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
function truncateBrowserUtf8Middle(value: string, maxBytes: number): string {
|
|
930
|
-
const bytes = encoder.encode(value);
|
|
931
|
-
if (bytes.byteLength <= maxBytes) return value;
|
|
932
|
-
const marker = `…[${bytes.byteLength - maxBytes} bytes omitted]…`;
|
|
933
|
-
const contentBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
|
|
934
|
-
const leftBudget = Math.floor(contentBudget / 2);
|
|
935
|
-
const rightBudget = contentBudget - leftBudget;
|
|
936
|
-
let leftEnd = Math.min(leftBudget, bytes.byteLength);
|
|
937
|
-
while (leftEnd > 0 && leftEnd < bytes.byteLength && isBrowserUtf8Continuation(bytes[leftEnd]!)) {
|
|
938
|
-
leftEnd -= 1;
|
|
939
|
-
}
|
|
940
|
-
let rightStart = Math.max(0, bytes.byteLength - rightBudget);
|
|
941
|
-
while (rightStart < bytes.byteLength && isBrowserUtf8Continuation(bytes[rightStart]!)) {
|
|
942
|
-
rightStart += 1;
|
|
943
|
-
}
|
|
944
|
-
return `${decoder.decode(bytes.subarray(0, leftEnd))}${marker}${decoder.decode(bytes.subarray(rightStart))}`;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
function settleBrowserEventTruncation(
|
|
948
|
-
event: SessionEvent,
|
|
949
|
-
truncation: {
|
|
950
|
-
originalBytes: number | null;
|
|
951
|
-
deliveredBytes: number;
|
|
952
|
-
omittedBytes: number | null;
|
|
953
|
-
estimatedDeliveredTokens: number;
|
|
954
|
-
},
|
|
955
|
-
): void {
|
|
956
|
-
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
957
|
-
const deliveredBytes = browserJsonBytes(event);
|
|
958
|
-
const omittedBytes =
|
|
959
|
-
truncation.originalBytes === null
|
|
960
|
-
? null
|
|
961
|
-
: Math.max(0, truncation.originalBytes - deliveredBytes);
|
|
962
|
-
const estimatedDeliveredTokens = Math.ceil(deliveredBytes / 4);
|
|
963
|
-
if (
|
|
964
|
-
truncation.deliveredBytes === deliveredBytes &&
|
|
965
|
-
truncation.omittedBytes === omittedBytes &&
|
|
966
|
-
truncation.estimatedDeliveredTokens === estimatedDeliveredTokens
|
|
967
|
-
) {
|
|
968
|
-
return;
|
|
969
|
-
}
|
|
970
|
-
truncation.deliveredBytes = deliveredBytes;
|
|
971
|
-
truncation.omittedBytes = omittedBytes;
|
|
972
|
-
truncation.estimatedDeliveredTokens = estimatedDeliveredTokens;
|
|
973
|
-
}
|
|
974
|
-
throw new RangeError("Browser event byte accounting did not converge");
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
function browserUtf8Bytes(value: string): number {
|
|
978
|
-
return encoder.encode(value).byteLength;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
function isBrowserUtf8Continuation(value: number): boolean {
|
|
982
|
-
return (value & 0xc0) === 0x80;
|
|
1192
|
+
function browserJsonBytes(value: unknown): number {
|
|
1193
|
+
return encoder.encode(JSON.stringify(value)).byteLength;
|
|
983
1194
|
}
|
|
984
1195
|
|
|
985
1196
|
function observeSessionStatus(
|
|
986
1197
|
events: readonly SessionEvent[],
|
|
987
1198
|
ref: { current: { sequence: number; status: SessionStatus | null } },
|
|
988
|
-
|
|
989
|
-
): void {
|
|
1199
|
+
): SessionStatus | undefined {
|
|
990
1200
|
let latest: { sequence: number; status: SessionStatus } | null = null;
|
|
991
1201
|
for (const event of events) {
|
|
992
1202
|
const status = sessionStatusFromEvents([event]);
|
|
@@ -994,9 +1204,71 @@ function observeSessionStatus(
|
|
|
994
1204
|
latest = { sequence: event.sequence, status };
|
|
995
1205
|
}
|
|
996
1206
|
}
|
|
997
|
-
if (!latest || latest.sequence < ref.current.sequence) return;
|
|
1207
|
+
if (!latest || latest.sequence < ref.current.sequence) return undefined;
|
|
998
1208
|
ref.current = latest;
|
|
999
|
-
|
|
1209
|
+
return latest.status;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
type ForegroundCatchupPlan =
|
|
1213
|
+
| { kind: "resume" }
|
|
1214
|
+
| { kind: "append"; events: SessionEvent[]; resumeSequence: number }
|
|
1215
|
+
| { kind: "reload" };
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Decide how a sustained hidden-tab suspension rejoins the durable event log.
|
|
1219
|
+
*
|
|
1220
|
+
* `lastSequence - cursor` is the exact raw durable work missed, but it is not
|
|
1221
|
+
* the number of visible messages: one streaming answer can own thousands of
|
|
1222
|
+
* adjacent delta rows. Small raw gaps therefore resume directly; medium gaps
|
|
1223
|
+
* get one forward compact page and are judged by the browser-visible result;
|
|
1224
|
+
* a page that cannot prove complete bounded coverage, or that would add too
|
|
1225
|
+
* many rows/groups/bytes, reloads the latest tail instead.
|
|
1226
|
+
*/
|
|
1227
|
+
async function planForegroundCatchup(
|
|
1228
|
+
client: EmbeddedSessionClientLike,
|
|
1229
|
+
workspaceId: string,
|
|
1230
|
+
sessionId: string,
|
|
1231
|
+
cursor: number,
|
|
1232
|
+
options: { signal?: AbortSignal } = {},
|
|
1233
|
+
): Promise<ForegroundCatchupPlan> {
|
|
1234
|
+
if (options.signal?.aborted) throw abortError();
|
|
1235
|
+
const session = await client.getSession(workspaceId, sessionId, {
|
|
1236
|
+
fresh: true,
|
|
1237
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
1238
|
+
});
|
|
1239
|
+
if (options.signal?.aborted) throw abortError();
|
|
1240
|
+
const durableHead = Math.max(cursor, session.lastSequence);
|
|
1241
|
+
const rawGap = durableHead - cursor;
|
|
1242
|
+
if (rawGap <= FOREGROUND_DIRECT_REPLAY_MAX_SEQUENCES) {
|
|
1243
|
+
return { kind: "resume" };
|
|
1244
|
+
}
|
|
1245
|
+
if (rawGap > FOREGROUND_COMPACT_PROBE_MAX_SEQUENCES) {
|
|
1246
|
+
return { kind: "reload" };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const page = await loadNextPage(client, workspaceId, sessionId, cursor, {
|
|
1250
|
+
pageSize: rawGap,
|
|
1251
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
1252
|
+
});
|
|
1253
|
+
const events = page.filter((event) => eventResumeSequence(event) > cursor);
|
|
1254
|
+
if (events.length === 0) {
|
|
1255
|
+
return { kind: "reload" };
|
|
1256
|
+
}
|
|
1257
|
+
assertAscending(events);
|
|
1258
|
+
const resumeSequence = maxResumeSequence(events);
|
|
1259
|
+
if (resumeSequence < durableHead) {
|
|
1260
|
+
// Byte/count truncation or an incomplete projection means this page cannot
|
|
1261
|
+
// prove that one append reaches the head observed above.
|
|
1262
|
+
return { kind: "reload" };
|
|
1263
|
+
}
|
|
1264
|
+
if (
|
|
1265
|
+
events.length > FOREGROUND_COMPACT_CATCHUP_MAX_EVENTS ||
|
|
1266
|
+
groupCount(events) > FOREGROUND_COMPACT_CATCHUP_MAX_GROUPS ||
|
|
1267
|
+
browserJsonBytes(events) > FOREGROUND_COMPACT_CATCHUP_MAX_BYTES
|
|
1268
|
+
) {
|
|
1269
|
+
return { kind: "reload" };
|
|
1270
|
+
}
|
|
1271
|
+
return { kind: "append", events, resumeSequence };
|
|
1000
1272
|
}
|
|
1001
1273
|
|
|
1002
1274
|
type LoadedEventWindow = {
|
|
@@ -1022,7 +1294,9 @@ async function loadEventWindow(
|
|
|
1022
1294
|
pageSize: number;
|
|
1023
1295
|
targetGroups: number;
|
|
1024
1296
|
maxFetches: number;
|
|
1297
|
+
boundaryPageCap?: number;
|
|
1025
1298
|
signal?: AbortSignal;
|
|
1299
|
+
isCurrent?: () => boolean;
|
|
1026
1300
|
},
|
|
1027
1301
|
): Promise<LoadedEventWindow> {
|
|
1028
1302
|
let cursor = options.before;
|
|
@@ -1038,6 +1312,7 @@ async function loadEventWindow(
|
|
|
1038
1312
|
pageSize: options.pageSize,
|
|
1039
1313
|
...(options.signal ? { signal: options.signal } : {}),
|
|
1040
1314
|
});
|
|
1315
|
+
if (options.isCurrent?.() === false) throw abortError();
|
|
1041
1316
|
fetches += 1;
|
|
1042
1317
|
if (page.length === 0) {
|
|
1043
1318
|
reachedStart = true;
|
|
@@ -1056,19 +1331,19 @@ async function loadEventWindow(
|
|
|
1056
1331
|
// turn boundary already in the buffer — the dropped fragment is refetched by
|
|
1057
1332
|
// the next loadOlder (everything below the new oldest sequence), whose own
|
|
1058
1333
|
// window snaps the same way, so every seam lands on a turn start. Extra
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1334
|
+
// page is fetched only when the buffer holds no boundary at all (one dense
|
|
1335
|
+
// turn); past the cap the existing truncation/hasOlder signal remains true.
|
|
1061
1336
|
let snapPages = 0;
|
|
1062
1337
|
while (
|
|
1063
1338
|
!reachedStart &&
|
|
1064
1339
|
findBoundaryIndex(buffer) === -1 &&
|
|
1065
|
-
snapPages <
|
|
1066
|
-
fetches < options.maxFetches
|
|
1340
|
+
snapPages < (options.boundaryPageCap ?? 0)
|
|
1067
1341
|
) {
|
|
1068
1342
|
const page = await loadPreviousPage(client, workspaceId, sessionId, cursor, {
|
|
1069
1343
|
pageSize: options.pageSize,
|
|
1070
1344
|
...(options.signal ? { signal: options.signal } : {}),
|
|
1071
1345
|
});
|
|
1346
|
+
if (options.isCurrent?.() === false) throw abortError();
|
|
1072
1347
|
fetches += 1;
|
|
1073
1348
|
snapPages += 1;
|
|
1074
1349
|
if (page.length === 0) {
|
|
@@ -1089,6 +1364,8 @@ async function loadEventWindow(
|
|
|
1089
1364
|
buffer = buffer.slice(boundary);
|
|
1090
1365
|
}
|
|
1091
1366
|
}
|
|
1367
|
+
const densityWindow = trimBackwardToGroupTarget(buffer, options.targetGroups);
|
|
1368
|
+
buffer = densityWindow.events;
|
|
1092
1369
|
|
|
1093
1370
|
const oldest = buffer[0] ?? null;
|
|
1094
1371
|
const newest = buffer[buffer.length - 1] ?? null;
|
|
@@ -1096,21 +1373,26 @@ async function loadEventWindow(
|
|
|
1096
1373
|
events: buffer,
|
|
1097
1374
|
oldestSequence: oldest?.sequence ?? null,
|
|
1098
1375
|
newestSequence: newest ? maxResumeSequence(buffer) : 0,
|
|
1099
|
-
hasOlder:
|
|
1376
|
+
hasOlder:
|
|
1377
|
+
buffer.length > 0 &&
|
|
1378
|
+
(densityWindow.trimmed || (!reachedStart && oldest?.type !== "session.created")),
|
|
1100
1379
|
};
|
|
1101
1380
|
}
|
|
1102
1381
|
|
|
1103
1382
|
/** Index of the oldest clean turn start in the buffer, or -1. */
|
|
1104
1383
|
function findBoundaryIndex(events: SessionEvent[]): number {
|
|
1105
1384
|
for (let index = 0; index < events.length; index += 1) {
|
|
1106
|
-
|
|
1107
|
-
if (type === "session.created" || type === "user.message") {
|
|
1385
|
+
if (isTurnBoundary(events[index]!)) {
|
|
1108
1386
|
return index;
|
|
1109
1387
|
}
|
|
1110
1388
|
}
|
|
1111
1389
|
return -1;
|
|
1112
1390
|
}
|
|
1113
1391
|
|
|
1392
|
+
function isTurnBoundary(event: SessionEvent): boolean {
|
|
1393
|
+
return event.type === "session.created" || event.type === "user.message";
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1114
1396
|
/**
|
|
1115
1397
|
* Forward (oldest→newer) density-bounded window. Used for jump-to-start and
|
|
1116
1398
|
* loadNewer so the browser never walks the whole middle gap.
|
|
@@ -1125,6 +1407,7 @@ async function loadForwardEventWindow(
|
|
|
1125
1407
|
targetGroups: number;
|
|
1126
1408
|
maxFetches: number;
|
|
1127
1409
|
signal?: AbortSignal;
|
|
1410
|
+
isCurrent?: () => boolean;
|
|
1128
1411
|
},
|
|
1129
1412
|
): Promise<LoadedForwardEventWindow> {
|
|
1130
1413
|
let cursor = options.after;
|
|
@@ -1140,6 +1423,7 @@ async function loadForwardEventWindow(
|
|
|
1140
1423
|
pageSize: options.pageSize,
|
|
1141
1424
|
...(options.signal ? { signal: options.signal } : {}),
|
|
1142
1425
|
});
|
|
1426
|
+
if (options.isCurrent?.() === false) throw abortError();
|
|
1143
1427
|
fetches += 1;
|
|
1144
1428
|
if (page.length === 0) {
|
|
1145
1429
|
reachedEnd = true;
|
|
@@ -1147,12 +1431,15 @@ async function loadForwardEventWindow(
|
|
|
1147
1431
|
}
|
|
1148
1432
|
assertAscending(page);
|
|
1149
1433
|
buffer = [...buffer, ...page];
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1434
|
+
// Compact events retain their first raw sequence in `sequence` and expose
|
|
1435
|
+
// the covered high-water mark through `coalescedUntil`. Advancing by the
|
|
1436
|
+
// first sequence duplicates compacted text on the next page. A compact
|
|
1437
|
+
// response length is not an end-of-history signal either: thousands of raw
|
|
1438
|
+
// events may legitimately collapse into one returned event.
|
|
1439
|
+
cursor = maxResumeSequence(page);
|
|
1155
1440
|
}
|
|
1441
|
+
const densityWindow = trimForwardToGroupTarget(buffer, options.targetGroups);
|
|
1442
|
+
buffer = densityWindow.events;
|
|
1156
1443
|
|
|
1157
1444
|
const oldest = buffer[0] ?? null;
|
|
1158
1445
|
const newest = buffer[buffer.length - 1] ?? null;
|
|
@@ -1160,10 +1447,45 @@ async function loadForwardEventWindow(
|
|
|
1160
1447
|
events: buffer,
|
|
1161
1448
|
oldestSequence: oldest?.sequence ?? null,
|
|
1162
1449
|
newestSequence: newest ? maxResumeSequence(buffer) : 0,
|
|
1163
|
-
hasNewer: buffer.length > 0 && !reachedEnd,
|
|
1450
|
+
hasNewer: buffer.length > 0 && (densityWindow.trimmed || !reachedEnd),
|
|
1164
1451
|
};
|
|
1165
1452
|
}
|
|
1166
1453
|
|
|
1454
|
+
type DensityWindow = {
|
|
1455
|
+
events: SessionEvent[];
|
|
1456
|
+
trimmed: boolean;
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
/** Keep the newest complete turn groups near an older-history seam. */
|
|
1460
|
+
function trimBackwardToGroupTarget(events: SessionEvent[], targetGroups: number): DensityWindow {
|
|
1461
|
+
if (!Number.isFinite(targetGroups) || targetGroups <= 0 || events.length === 0) {
|
|
1462
|
+
return { events, trimmed: false };
|
|
1463
|
+
}
|
|
1464
|
+
for (let index = events.length - 1; index > 0; index -= 1) {
|
|
1465
|
+
if (!isTurnBoundary(events[index]!)) continue;
|
|
1466
|
+
const candidate = events.slice(index);
|
|
1467
|
+
if (groupCount(candidate) >= targetGroups) {
|
|
1468
|
+
return { events: candidate, trimmed: true };
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
return { events, trimmed: false };
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/** Keep the oldest complete turn groups near a forward-history seam. */
|
|
1475
|
+
function trimForwardToGroupTarget(events: SessionEvent[], targetGroups: number): DensityWindow {
|
|
1476
|
+
if (!Number.isFinite(targetGroups) || targetGroups <= 0 || events.length === 0) {
|
|
1477
|
+
return { events, trimmed: false };
|
|
1478
|
+
}
|
|
1479
|
+
for (let index = 1; index < events.length; index += 1) {
|
|
1480
|
+
if (!isTurnBoundary(events[index]!)) continue;
|
|
1481
|
+
const candidate = events.slice(0, index);
|
|
1482
|
+
if (groupCount(candidate) >= targetGroups) {
|
|
1483
|
+
return { events: candidate, trimmed: true };
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
return { events, trimmed: false };
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1167
1489
|
type PreviousPage = SessionEvent[] & { requested: number };
|
|
1168
1490
|
|
|
1169
1491
|
async function loadPreviousPage(
|
|
@@ -1240,18 +1562,22 @@ function isLogStart(event: SessionEvent): boolean {
|
|
|
1240
1562
|
return event.type === "session.created" || event.sequence <= 1;
|
|
1241
1563
|
}
|
|
1242
1564
|
|
|
1243
|
-
function maxResumeSequence(events: SessionEvent[]): number {
|
|
1565
|
+
function maxResumeSequence(events: readonly SessionEvent[]): number {
|
|
1244
1566
|
return events.reduce((max, event) => Math.max(max, eventResumeSequence(event)), 0);
|
|
1245
1567
|
}
|
|
1246
1568
|
|
|
1247
|
-
function
|
|
1248
|
-
|
|
1249
|
-
const coalescedUntil = Number(payload.coalescedUntil);
|
|
1250
|
-
return Math.max(event.sequence, Number.isFinite(coalescedUntil) ? Math.floor(coalescedUntil) : 0);
|
|
1569
|
+
function maxResumeSequenceOrNull(events: readonly SessionEvent[]): number | null {
|
|
1570
|
+
return events.length > 0 ? maxResumeSequence(events) : null;
|
|
1251
1571
|
}
|
|
1252
1572
|
|
|
1253
|
-
function
|
|
1254
|
-
|
|
1573
|
+
function eventResumeSequence(event: SessionEvent): number {
|
|
1574
|
+
const streamedCoverage = sessionEventStreamCoveredThrough(event);
|
|
1575
|
+
if (streamedCoverage !== null) return streamedCoverage;
|
|
1576
|
+
return typeof event.coveredThrough === "number" &&
|
|
1577
|
+
Number.isSafeInteger(event.coveredThrough) &&
|
|
1578
|
+
event.coveredThrough >= event.sequence
|
|
1579
|
+
? event.coveredThrough
|
|
1580
|
+
: event.sequence;
|
|
1255
1581
|
}
|
|
1256
1582
|
|
|
1257
1583
|
function assertAscending(events: SessionEvent[]): void {
|