@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,13 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parseMediaGenerationResult,
|
|
3
|
+
parseToolDisplayMetadata,
|
|
4
|
+
EMPTY_FINAL_REPLY_NOTICE,
|
|
5
|
+
turnCompletedWithEmptyFinalReply,
|
|
6
|
+
type HumanInputAnswer,
|
|
7
|
+
type HumanInputQuestion,
|
|
8
|
+
type HumanInputResponse,
|
|
3
9
|
type SessionEvent,
|
|
4
10
|
type SessionStatus,
|
|
5
11
|
type TimelineAnnotation,
|
|
6
12
|
} from "@opengeni/sdk";
|
|
7
13
|
import fleetDecisionItem from "./fleet-decision-projection";
|
|
14
|
+
import {
|
|
15
|
+
allowanceExhaustedMessage,
|
|
16
|
+
parseAllowanceExhaustedRefusal,
|
|
17
|
+
} from "@opengeni/sdk/allowance-refusal";
|
|
8
18
|
import {
|
|
9
19
|
CREDIT_EXHAUSTION_MESSAGE,
|
|
10
|
-
|
|
20
|
+
presentFailure,
|
|
11
21
|
isCreditExhaustion,
|
|
12
22
|
tryParseJson,
|
|
13
23
|
} from "../lib/format";
|
|
@@ -18,9 +28,15 @@ import type {
|
|
|
18
28
|
AuthNeededItem,
|
|
19
29
|
ContextCompactionItem,
|
|
20
30
|
GoalItem,
|
|
31
|
+
HumanInputAnswerSummary,
|
|
32
|
+
HumanInputItem,
|
|
21
33
|
MachineInputBatchItem,
|
|
34
|
+
MachineInputMember,
|
|
22
35
|
MemoryItem,
|
|
36
|
+
NoticeItem,
|
|
23
37
|
SandboxItem,
|
|
38
|
+
StartupPhase,
|
|
39
|
+
StartupPhaseItem,
|
|
24
40
|
SessionStatusItem,
|
|
25
41
|
TimelineGroup,
|
|
26
42
|
TimelineItem,
|
|
@@ -32,6 +48,14 @@ import type {
|
|
|
32
48
|
|
|
33
49
|
export { toolDisplayName, mcpToolLeaf, toolMatchesLeaf } from "./tool-display-name";
|
|
34
50
|
|
|
51
|
+
/** Message identity only: queue placement needs the surrounding turn history. */
|
|
52
|
+
export function isTimelineUserQuestion(event: SessionEvent): boolean {
|
|
53
|
+
return (
|
|
54
|
+
event.type === "user.message" &&
|
|
55
|
+
workerCompletionPayload(asRecord(event.payload).childCompletion) === null
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
/* ----------------------------------------------------------------------------
|
|
36
60
|
Timeline projection
|
|
37
61
|
|
|
@@ -51,6 +75,8 @@ const WORKER_SPAWN_TOOL = "session_create";
|
|
|
51
75
|
const WORKER_MESSAGE_TOOL = "session_send_message";
|
|
52
76
|
const WORKER_FAILURE_CODE_MAX_LENGTH = 128;
|
|
53
77
|
const WORKER_FAILURE_MESSAGE_MAX_UTF8_BYTES = 1_024;
|
|
78
|
+
type PendingWaitOutcome = { id: string; reason: string; occurredAt: string };
|
|
79
|
+
type TrackedAgentResponse = { item: AgentMessageItem; completed: boolean };
|
|
54
80
|
|
|
55
81
|
/**
|
|
56
82
|
* Tools whose durable side-effect events already own the timeline (MemoryRow).
|
|
@@ -120,13 +146,199 @@ function timelineAnnotationToolOutputText(output: unknown): string | null {
|
|
|
120
146
|
return text.length > 0 ? text : null;
|
|
121
147
|
}
|
|
122
148
|
|
|
123
|
-
export function buildTimeline(
|
|
149
|
+
export function buildTimeline(
|
|
150
|
+
events: SessionEvent[],
|
|
151
|
+
options: { partialStart?: boolean } = {},
|
|
152
|
+
): TimelineItem[] {
|
|
124
153
|
const items: TimelineItem[] = [];
|
|
154
|
+
const allowanceNotices = new Set<string>();
|
|
155
|
+
const allowanceNotice = (event: SessionEvent, turnId: string | null) => {
|
|
156
|
+
const refusal = parseAllowanceExhaustedRefusal(event.payload);
|
|
157
|
+
if (!refusal) return null;
|
|
158
|
+
const text = allowanceExhaustedMessage(refusal);
|
|
159
|
+
const key = JSON.stringify([
|
|
160
|
+
turnId,
|
|
161
|
+
event.turnAttemptId,
|
|
162
|
+
startupRecoveryRevisionByTurn.get(turnId ?? "") ?? 0,
|
|
163
|
+
refusal.scope,
|
|
164
|
+
refusal.subjectId,
|
|
165
|
+
refusal.resetsAt,
|
|
166
|
+
]);
|
|
167
|
+
if (!allowanceNotices.has(key)) {
|
|
168
|
+
allowanceNotices.add(key);
|
|
169
|
+
items.push({
|
|
170
|
+
kind: "notice",
|
|
171
|
+
id: `${event.id}-allowance`,
|
|
172
|
+
tone: "failed",
|
|
173
|
+
text,
|
|
174
|
+
// The canonical sentence replaces upstream prose, so a host renderer
|
|
175
|
+
// reading `message` never shows a wrapper's private wording.
|
|
176
|
+
allowance: { ...refusal, message: text },
|
|
177
|
+
occurredAt: event.occurredAt,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return text;
|
|
181
|
+
};
|
|
125
182
|
const prescan = prescanTurnAnchors(events);
|
|
126
183
|
const ordered = orderTimelineEvents(events, prescan);
|
|
184
|
+
// A bounded replay can begin halfway through a message (including inside an
|
|
185
|
+
// interactive fence). Never parse that suffix as a complete Markdown source.
|
|
186
|
+
// Keep the raw events available for pagination; a completed receipt restores
|
|
187
|
+
// the authoritative message without requiring an unbounded history fetch.
|
|
188
|
+
let completeHistoryPrefix = options.partialStart !== true;
|
|
189
|
+
const knownMessagePrefixTurns = new Set<string | null>();
|
|
190
|
+
const incompleteMessageKeys = new Set<string>();
|
|
191
|
+
const pendingWaitOutcomeByTurn = new Map<string | null, PendingWaitOutcome>();
|
|
192
|
+
const latestAgentResponseByTurn = new Map<string | null, TrackedAgentResponse>();
|
|
193
|
+
const identifiedMessages = new Map<string, AgentMessageItem>();
|
|
194
|
+
const humanInputRequests = humanInputRequestsById(events);
|
|
195
|
+
const humanInputToolCallIds = new Set(
|
|
196
|
+
[...humanInputRequests.values()]
|
|
197
|
+
.map((request) => stringValue(request.toolCallId))
|
|
198
|
+
.filter(Boolean),
|
|
199
|
+
);
|
|
200
|
+
// Delegated workers that have not reported back yet, so a recorded agent wait
|
|
201
|
+
// can say how many agents it is waiting for. Only a session's own workers
|
|
202
|
+
// report back, so a message to any other session is never awaited. Best
|
|
203
|
+
// effort: a bounded replay only knows the spawns and results inside its window.
|
|
204
|
+
const workerSessions = new Set<string>();
|
|
205
|
+
const pendingWorkerSessions = new Set<string>();
|
|
206
|
+
let openWaitNotice: NoticeItem | null = null;
|
|
207
|
+
// Tool results update the original tool row in place, and running lifecycle
|
|
208
|
+
// receipts have no row. Carry resume evidence on the attention landmarks so
|
|
209
|
+
// grouping cannot mistake historical approval for a live wait.
|
|
210
|
+
const presentationWaits = new Map<string | null, Array<NoticeItem | SessionStatusItem>>();
|
|
211
|
+
const presentationFailures = new Map<string | null, SessionStatusItem[]>();
|
|
212
|
+
let presentationTurnId: string | null = null;
|
|
213
|
+
const rememberPresentationWait = (
|
|
214
|
+
item: NoticeItem | SessionStatusItem,
|
|
215
|
+
turnId: string | null,
|
|
216
|
+
) => {
|
|
217
|
+
const key = turnId ?? presentationTurnId;
|
|
218
|
+
const waits = presentationWaits.get(key) ?? [];
|
|
219
|
+
waits.push(item);
|
|
220
|
+
presentationWaits.set(key, waits);
|
|
221
|
+
items.push(item);
|
|
222
|
+
};
|
|
223
|
+
const resolvePresentationWait = (event: SessionEvent, turnId: string | null) => {
|
|
224
|
+
if (event.duplicateOfEventId || (event.turnAssociation && event.turnAssociation !== "current"))
|
|
225
|
+
return;
|
|
226
|
+
const key = turnId ?? presentationTurnId;
|
|
227
|
+
for (const item of presentationWaits.get(key) ?? []) item.resolvedAt = event.occurredAt;
|
|
228
|
+
presentationWaits.delete(key);
|
|
229
|
+
};
|
|
230
|
+
const queuedAtByTurn = new Map<string, string>();
|
|
231
|
+
const startupRecoveryRevisionByTurn = new Map<string, number>();
|
|
232
|
+
const startupPhases = new Map<string, readonly [StartupPhaseItem, string | null, number]>();
|
|
233
|
+
let startupEvent: SessionEvent;
|
|
234
|
+
let startupTurnId: string | null = null;
|
|
235
|
+
let startupAttemptId: string | null = null;
|
|
236
|
+
let startupRecoveryRevision = 0;
|
|
237
|
+
|
|
238
|
+
const settleStartupPhase = (
|
|
239
|
+
phase: StartupPhase,
|
|
240
|
+
status: StartupPhaseItem["status"],
|
|
241
|
+
duration: number | null,
|
|
242
|
+
outcome: StartupPhaseItem["outcome"],
|
|
243
|
+
mode = 0,
|
|
244
|
+
explicitStartedAt?: string,
|
|
245
|
+
explicitId?: string,
|
|
246
|
+
blockedReason?: StartupPhaseItem["blockedReason"],
|
|
247
|
+
): void => {
|
|
248
|
+
const key = `${startupTurnId}:${phase}`;
|
|
249
|
+
const priorState = startupPhases.get(key);
|
|
250
|
+
const prior = priorState?.[0];
|
|
251
|
+
const comparedByAttempt = priorState?.[1] && startupAttemptId;
|
|
252
|
+
const sameAttempt = comparedByAttempt
|
|
253
|
+
? priorState[1] === startupAttemptId
|
|
254
|
+
: (priorState?.[2] ?? 0) === startupRecoveryRevision;
|
|
255
|
+
const open = prior?.status === "running" && sameAttempt ? prior : undefined;
|
|
256
|
+
const replacesPriorAttempt =
|
|
257
|
+
prior !== undefined &&
|
|
258
|
+
(comparedByAttempt ? !sameAttempt : startupRecoveryRevision > (priorState?.[2] ?? 0));
|
|
259
|
+
// A later activity attempt may replay preparation that the logical turn had
|
|
260
|
+
// already completed before the worker moved. Keep that first successful
|
|
261
|
+
// landmark; only an abandoned or failed attempt needs replacement.
|
|
262
|
+
if (replacesPriorAttempt && prior.status === "complete") return;
|
|
263
|
+
if (mode === 2 && prior && !replacesPriorAttempt) return;
|
|
264
|
+
if (mode === 1 && !open) return;
|
|
265
|
+
if (!replacesPriorAttempt && status !== "running" && open) {
|
|
266
|
+
open.status = status;
|
|
267
|
+
open.completedAt = startupEvent.occurredAt;
|
|
268
|
+
open.durationMs =
|
|
269
|
+
duration === null
|
|
270
|
+
? elapsedDurationMs(open.startedAt, startupEvent.occurredAt)
|
|
271
|
+
: Math.max(0, duration);
|
|
272
|
+
open.outcome = outcome ?? open.outcome;
|
|
273
|
+
open.blockedReason = blockedReason;
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const running = status === "running";
|
|
277
|
+
const durationMs = running || duration === null ? null : Math.max(0, duration);
|
|
278
|
+
const completedEpoch = Date.parse(startupEvent.occurredAt);
|
|
279
|
+
const startedAt =
|
|
280
|
+
explicitStartedAt ??
|
|
281
|
+
(!running && durationMs !== null && Number.isFinite(completedEpoch)
|
|
282
|
+
? new Date(completedEpoch - durationMs).toISOString()
|
|
283
|
+
: startupEvent.occurredAt);
|
|
284
|
+
if (replacesPriorAttempt) {
|
|
285
|
+
Object.assign(prior, {
|
|
286
|
+
id: explicitId ?? startupEvent.id,
|
|
287
|
+
status,
|
|
288
|
+
startedAt,
|
|
289
|
+
completedAt: running ? null : startupEvent.occurredAt,
|
|
290
|
+
durationMs,
|
|
291
|
+
outcome,
|
|
292
|
+
blockedReason,
|
|
293
|
+
occurredAt: startedAt,
|
|
294
|
+
} satisfies Partial<StartupPhaseItem>);
|
|
295
|
+
startupPhases.set(key, [prior, startupAttemptId, startupRecoveryRevision]);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const item: StartupPhaseItem = {
|
|
299
|
+
kind: "startup-phase",
|
|
300
|
+
id: explicitId ?? startupEvent.id,
|
|
301
|
+
turnId: startupTurnId,
|
|
302
|
+
phase,
|
|
303
|
+
status,
|
|
304
|
+
startedAt,
|
|
305
|
+
completedAt: running ? null : startupEvent.occurredAt,
|
|
306
|
+
durationMs,
|
|
307
|
+
outcome,
|
|
308
|
+
blockedReason,
|
|
309
|
+
occurredAt: startedAt,
|
|
310
|
+
};
|
|
311
|
+
items.push(item);
|
|
312
|
+
startupPhases.set(key, [item, startupAttemptId, startupRecoveryRevision]);
|
|
313
|
+
};
|
|
127
314
|
|
|
128
315
|
const last = (): TimelineItem | undefined => items[items.length - 1];
|
|
129
316
|
|
|
317
|
+
const rememberAgentResponse = (
|
|
318
|
+
turnId: string | null,
|
|
319
|
+
item: AgentMessageItem,
|
|
320
|
+
completed: boolean,
|
|
321
|
+
): void => {
|
|
322
|
+
latestAgentResponseByTurn.set(turnId, { item, completed });
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const takePendingWaitOutcome = (turnId: string | null): PendingWaitOutcome | undefined => {
|
|
326
|
+
const outcome = pendingWaitOutcomeByTurn.get(turnId);
|
|
327
|
+
pendingWaitOutcomeByTurn.delete(turnId);
|
|
328
|
+
return outcome;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const takeAgentResponse = (turnId: string | null): TrackedAgentResponse | undefined => {
|
|
332
|
+
const response = latestAgentResponseByTurn.get(turnId);
|
|
333
|
+
latestAgentResponseByTurn.delete(turnId);
|
|
334
|
+
return response;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const clearUnscopedTerminalTracking = (): void => {
|
|
338
|
+
latestAgentResponseByTurn.delete(null);
|
|
339
|
+
pendingWaitOutcomeByTurn.delete(null);
|
|
340
|
+
};
|
|
341
|
+
|
|
130
342
|
/** A new item of a different kind ends whatever was streaming at the tail. */
|
|
131
343
|
const closeStreamingTail = (): void => {
|
|
132
344
|
const open = last();
|
|
@@ -138,6 +350,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
138
350
|
const finalizeOpen = (
|
|
139
351
|
turnId?: string | null,
|
|
140
352
|
disposition: "complete" | "failed" | "cancelled" = "complete",
|
|
353
|
+
completedAt?: string,
|
|
141
354
|
): void => {
|
|
142
355
|
for (const item of items) {
|
|
143
356
|
if (
|
|
@@ -158,20 +371,119 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
158
371
|
if (item.kind === "sandbox" && item.status === "running") {
|
|
159
372
|
item.status = disposition;
|
|
160
373
|
}
|
|
374
|
+
if (item.kind === "startup-phase" && item.status === "running") {
|
|
375
|
+
item.status = disposition;
|
|
376
|
+
if (completedAt) {
|
|
377
|
+
item.completedAt = completedAt;
|
|
378
|
+
item.durationMs = elapsedDurationMs(item.startedAt, completedAt);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
161
381
|
}
|
|
162
382
|
};
|
|
163
383
|
|
|
164
384
|
for (const event of ordered) {
|
|
165
385
|
const payload = asRecord(event.payload);
|
|
166
386
|
const turnId = event.turnId ?? null;
|
|
387
|
+
if (
|
|
388
|
+
turnId &&
|
|
389
|
+
(event.type === "turn.started" ||
|
|
390
|
+
(presentationTurnId === null && isAgentActivityEvent(event.type)))
|
|
391
|
+
) {
|
|
392
|
+
presentationTurnId = turnId;
|
|
393
|
+
}
|
|
394
|
+
if (
|
|
395
|
+
openWaitNotice &&
|
|
396
|
+
(event.type === "user.message" ||
|
|
397
|
+
event.type === "system.update.delivered" ||
|
|
398
|
+
event.type === "turn.started" ||
|
|
399
|
+
event.type === "session.control.paused" ||
|
|
400
|
+
(event.type === "session.status.changed" &&
|
|
401
|
+
(payload.status === "cancelled" || payload.status === "failed")))
|
|
402
|
+
) {
|
|
403
|
+
// New input, a pause, or the end of the session ends a recorded wait; the
|
|
404
|
+
// notice keeps its wait span.
|
|
405
|
+
openWaitNotice.waitEndedAt = event.occurredAt;
|
|
406
|
+
openWaitNotice = null;
|
|
407
|
+
}
|
|
408
|
+
if (event.type === "session.created") {
|
|
409
|
+
completeHistoryPrefix = true;
|
|
410
|
+
} else if (
|
|
411
|
+
event.type === "turn.started" ||
|
|
412
|
+
(event.type === "user.message" &&
|
|
413
|
+
payload.delivery !== "steer" &&
|
|
414
|
+
payload.routing !== "accepted_for_steering")
|
|
415
|
+
) {
|
|
416
|
+
// Accepted Steer is visible before its replacement turn starts. It says
|
|
417
|
+
// nothing about the prefix of a still-streaming message from the old turn.
|
|
418
|
+
knownMessagePrefixTurns.add(turnId);
|
|
419
|
+
}
|
|
420
|
+
startupEvent = event;
|
|
421
|
+
startupTurnId = turnId;
|
|
422
|
+
startupAttemptId =
|
|
423
|
+
(typeof event.turnAttemptId === "string" ? event.turnAttemptId : null) ??
|
|
424
|
+
stringValue(payload.attemptId);
|
|
425
|
+
startupRecoveryRevision = turnId ? (startupRecoveryRevisionByTurn.get(turnId) ?? 0) : 0;
|
|
426
|
+
const setupPhase = event.type.startsWith("rig.setup.")
|
|
427
|
+
? "rig"
|
|
428
|
+
: event.type.startsWith("turn.startup.phase.")
|
|
429
|
+
? startupPhaseFromPayload(payload.phase)
|
|
430
|
+
: null;
|
|
431
|
+
const setupStatus = event.type.endsWith(".started")
|
|
432
|
+
? "running"
|
|
433
|
+
: event.type.endsWith(".failed")
|
|
434
|
+
? "failed"
|
|
435
|
+
: event.type.endsWith(".completed") || event.type === "rig.setup.skipped"
|
|
436
|
+
? "complete"
|
|
437
|
+
: null;
|
|
438
|
+
if (setupPhase && setupStatus) {
|
|
439
|
+
closeStreamingTail();
|
|
440
|
+
let blockedReason: StartupPhaseItem["blockedReason"];
|
|
441
|
+
if (setupPhase === "model_preparation" && setupStatus === "failed") {
|
|
442
|
+
blockedReason = startupLifecycleWaitReason(payload);
|
|
443
|
+
// Older preparation events omit the typed lifecycle fields. The
|
|
444
|
+
// sandbox span can supply them only inside the same turn attempt.
|
|
445
|
+
const sandbox = startupPhases.get(`${startupTurnId}:sandbox`);
|
|
446
|
+
const sameAttempt =
|
|
447
|
+
sandbox?.[1] || startupAttemptId
|
|
448
|
+
? Boolean(startupAttemptId && sandbox?.[1] === startupAttemptId)
|
|
449
|
+
: sandbox?.[2] === startupRecoveryRevision;
|
|
450
|
+
if (
|
|
451
|
+
!blockedReason &&
|
|
452
|
+
payload.expectedTransition === undefined &&
|
|
453
|
+
payload.failureCode === undefined &&
|
|
454
|
+
payload.failureStage === undefined &&
|
|
455
|
+
payload.failureCategory === undefined &&
|
|
456
|
+
payload.error === undefined &&
|
|
457
|
+
startupTurnId &&
|
|
458
|
+
sameAttempt &&
|
|
459
|
+
sandbox?.[0].status === "cancelled"
|
|
460
|
+
) {
|
|
461
|
+
blockedReason = sandbox[0].blockedReason;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
settleStartupPhase(
|
|
465
|
+
setupPhase,
|
|
466
|
+
blockedReason ? "cancelled" : setupStatus,
|
|
467
|
+
numberOrNull(payload.durationMs),
|
|
468
|
+
event.type === "rig.setup.skipped" ? "skipped" : null,
|
|
469
|
+
0,
|
|
470
|
+
undefined,
|
|
471
|
+
undefined,
|
|
472
|
+
blockedReason,
|
|
473
|
+
);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
167
476
|
|
|
168
477
|
switch (event.type) {
|
|
169
478
|
case "user.message": {
|
|
170
479
|
// A steering message must not mark in-flight tools complete; it only
|
|
171
480
|
// ends whatever text was streaming. Turn lifecycle events finalize.
|
|
481
|
+
clearUnscopedTerminalTracking();
|
|
172
482
|
closeStreamingTail();
|
|
173
483
|
const childCompletion = workerCompletionPayload(payload.childCompletion);
|
|
174
484
|
if (childCompletion) {
|
|
485
|
+
workerSessions.add(childCompletion.childSessionId);
|
|
486
|
+
pendingWorkerSessions.delete(childCompletion.childSessionId);
|
|
175
487
|
items.push({
|
|
176
488
|
kind: "worker-completion",
|
|
177
489
|
id: event.id,
|
|
@@ -191,6 +503,9 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
191
503
|
items.push({
|
|
192
504
|
kind: "user-message",
|
|
193
505
|
id: event.id,
|
|
506
|
+
...(event.clientEventId
|
|
507
|
+
? { reconciliationKey: `user-message:${event.clientEventId}` }
|
|
508
|
+
: {}),
|
|
194
509
|
text: voiceMessage?.text ?? stringValue(payload.text),
|
|
195
510
|
annotations: timelineAnnotations(payload.annotations),
|
|
196
511
|
annotationSource: {
|
|
@@ -209,13 +524,38 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
209
524
|
break;
|
|
210
525
|
}
|
|
211
526
|
|
|
527
|
+
case "user.humanInputResponse": {
|
|
528
|
+
// A structured answer resumes the frozen tool call rather than adding a
|
|
529
|
+
// synthetic user.message to model history. Keep the original question
|
|
530
|
+
// and the resolved human choice together as first-class chat history;
|
|
531
|
+
// neither belongs behind the generic activity fold.
|
|
532
|
+
const item = humanInputItemFromResponse(event, payload, humanInputRequests);
|
|
533
|
+
if (!item) break;
|
|
534
|
+
closeStreamingTail();
|
|
535
|
+
items.push(item);
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
|
|
212
539
|
case "system.update.delivered": {
|
|
213
540
|
const inputs = machineInputMembers(payload.members);
|
|
541
|
+
for (const input of inputs) {
|
|
542
|
+
if (input.kind === "child_terminal_result") {
|
|
543
|
+
workerSessions.add(input.sourceId);
|
|
544
|
+
pendingWorkerSessions.delete(input.sourceId);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
214
547
|
if (inputs.length === 0) break;
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
548
|
+
// Background command receipts are agent-facing inputs, not chat messages.
|
|
549
|
+
// The durable event remains available to the agent and audit history.
|
|
550
|
+
const visibleInputs = inputs.filter(
|
|
551
|
+
(member) => member.kind !== "background_command_result",
|
|
552
|
+
);
|
|
553
|
+
// Goal continuations already have GoalRows. A batch with no other
|
|
554
|
+
// visible update must not interrupt an in-flight streaming message.
|
|
555
|
+
if (
|
|
556
|
+
visibleInputs.length === 0 ||
|
|
557
|
+
visibleInputs.every((member) => member.kind === "goal_continuation")
|
|
558
|
+
) {
|
|
219
559
|
break;
|
|
220
560
|
}
|
|
221
561
|
closeStreamingTail();
|
|
@@ -223,7 +563,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
223
563
|
kind: "machine-input-batch",
|
|
224
564
|
id: event.id,
|
|
225
565
|
turnId,
|
|
226
|
-
members:
|
|
566
|
+
members: visibleInputs,
|
|
227
567
|
occurredAt: event.occurredAt,
|
|
228
568
|
});
|
|
229
569
|
break;
|
|
@@ -234,30 +574,85 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
234
574
|
if (!text) {
|
|
235
575
|
break;
|
|
236
576
|
}
|
|
577
|
+
const messageId = stringValue(payload.messageId);
|
|
578
|
+
const messageKey = messageId ? JSON.stringify([turnId, messageId]) : null;
|
|
579
|
+
// Responses providers declare the phase when they announce the item, so
|
|
580
|
+
// a streaming progress note is known as commentary before it completes.
|
|
581
|
+
const phase = assistantMessagePhase(payload.phase);
|
|
582
|
+
if (
|
|
583
|
+
(!completeHistoryPrefix && !knownMessagePrefixTurns.has(turnId)) ||
|
|
584
|
+
(messageKey && incompleteMessageKeys.has(messageKey))
|
|
585
|
+
) {
|
|
586
|
+
if (messageKey) incompleteMessageKeys.add(messageKey);
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
const identified = messageKey ? identifiedMessages.get(messageKey) : undefined;
|
|
590
|
+
if (identified) {
|
|
591
|
+
if (identified.annotationSource?.eventType !== "agent.message.completed") {
|
|
592
|
+
identified.text += text;
|
|
593
|
+
if (phase && !identified.phase) identified.phase = phase;
|
|
594
|
+
rememberAgentResponse(turnId, identified, false);
|
|
595
|
+
}
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
237
598
|
const open = last();
|
|
238
|
-
if (open?.kind === "
|
|
599
|
+
if (!messageKey && open?.kind === "tool-call" && open.status === "running") {
|
|
600
|
+
const previous = latestAgentResponseByTurn.get(turnId);
|
|
601
|
+
const previousIndex = previous ? items.indexOf(previous.item) : -1;
|
|
602
|
+
if (
|
|
603
|
+
previous &&
|
|
604
|
+
!previous.completed &&
|
|
605
|
+
previousIndex >= 0 &&
|
|
606
|
+
items
|
|
607
|
+
.slice(previousIndex + 1)
|
|
608
|
+
.every(
|
|
609
|
+
(item) =>
|
|
610
|
+
item.kind === "tool-call" && item.turnId === turnId && item.status === "running",
|
|
611
|
+
)
|
|
612
|
+
) {
|
|
613
|
+
// Legacy deltas have no provider message identity. Tool creation
|
|
614
|
+
// alone is not a text boundary; completed output still is.
|
|
615
|
+
previous.item.text += text;
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (
|
|
620
|
+
!messageKey &&
|
|
621
|
+
open?.kind === "agent-message" &&
|
|
622
|
+
open.streaming &&
|
|
623
|
+
open.turnId === turnId
|
|
624
|
+
) {
|
|
239
625
|
open.text += text;
|
|
626
|
+
rememberAgentResponse(turnId, open, false);
|
|
240
627
|
break;
|
|
241
628
|
}
|
|
242
629
|
closeStreamingTail();
|
|
243
|
-
|
|
630
|
+
const item: AgentMessageItem = {
|
|
244
631
|
kind: "agent-message",
|
|
245
632
|
id: event.id,
|
|
246
633
|
turnId,
|
|
247
634
|
text,
|
|
635
|
+
...(phase ? { phase } : {}),
|
|
248
636
|
streaming: true,
|
|
249
637
|
occurredAt: event.occurredAt,
|
|
250
|
-
}
|
|
638
|
+
};
|
|
639
|
+
items.push(item);
|
|
640
|
+
if (messageKey) identifiedMessages.set(messageKey, item);
|
|
641
|
+
rememberAgentResponse(turnId, item, false);
|
|
251
642
|
break;
|
|
252
643
|
}
|
|
253
644
|
|
|
254
645
|
case "agent.message.completed": {
|
|
646
|
+
knownMessagePrefixTurns.add(turnId);
|
|
255
647
|
const text = stringValue(payload.text);
|
|
648
|
+
const messageId = stringValue(payload.messageId);
|
|
649
|
+
const messageKey = messageId ? JSON.stringify([turnId, messageId]) : null;
|
|
650
|
+
const phase = assistantMessagePhase(payload.phase);
|
|
256
651
|
// Reconcile the most recent same-turn agent message — even when
|
|
257
652
|
// activity (tool calls, reasoning) landed after its deltas — so the
|
|
258
653
|
// completed text never duplicates the streamed one.
|
|
259
|
-
let openIndex = -1;
|
|
260
|
-
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
654
|
+
let openIndex = messageKey ? items.indexOf(identifiedMessages.get(messageKey)!) : -1;
|
|
655
|
+
for (let index = messageKey ? -1 : items.length - 1; index >= 0; index -= 1) {
|
|
261
656
|
const candidate = items[index];
|
|
262
657
|
if (candidate?.kind === "agent-message" && candidate.turnId === turnId) {
|
|
263
658
|
openIndex = index;
|
|
@@ -269,15 +664,26 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
269
664
|
candidate?.kind === "agent-message" ? candidate : undefined;
|
|
270
665
|
if (
|
|
271
666
|
open &&
|
|
272
|
-
(
|
|
667
|
+
(messageKey ||
|
|
668
|
+
open.streaming ||
|
|
669
|
+
!open.text ||
|
|
670
|
+
text === open.text ||
|
|
671
|
+
text.startsWith(open.text))
|
|
273
672
|
) {
|
|
274
|
-
//
|
|
275
|
-
|
|
673
|
+
// Identity makes the final text authoritative even if it corrects the
|
|
674
|
+
// draft; legacy receipts still require an extension match.
|
|
675
|
+
if (!open.text || (text && (messageKey || text.startsWith(open.text)))) {
|
|
276
676
|
open.text = text || open.text;
|
|
277
677
|
}
|
|
678
|
+
// A phase-less settlement mirror (the worker's final-output receipt)
|
|
679
|
+
// must not erase the provider-declared phase captured by the SDK item.
|
|
680
|
+
if (phase) open.phase = phase;
|
|
681
|
+
// Completion time is what the footer shows ("finished at"); the
|
|
682
|
+
// first-delta stamp stays as the stream's start.
|
|
683
|
+
if (open.startedAt === undefined && open.occurredAt !== event.occurredAt) {
|
|
684
|
+
open.startedAt = open.occurredAt;
|
|
685
|
+
}
|
|
278
686
|
open.streaming = false;
|
|
279
|
-
// Completion time is what the footer shows ("finished at"); keep the
|
|
280
|
-
// first-delta stamp only until this event arrives.
|
|
281
687
|
open.occurredAt = event.occurredAt;
|
|
282
688
|
open.annotationSource =
|
|
283
689
|
text && open.text === text
|
|
@@ -299,14 +705,16 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
299
705
|
items.splice(openIndex, 1);
|
|
300
706
|
items.push(open);
|
|
301
707
|
}
|
|
708
|
+
rememberAgentResponse(turnId, open, true);
|
|
302
709
|
break;
|
|
303
710
|
}
|
|
304
711
|
if (text) {
|
|
305
|
-
|
|
712
|
+
const item: AgentMessageItem = {
|
|
306
713
|
kind: "agent-message",
|
|
307
714
|
id: event.id,
|
|
308
715
|
turnId,
|
|
309
716
|
text,
|
|
717
|
+
...(phase ? { phase } : {}),
|
|
310
718
|
streaming: false,
|
|
311
719
|
occurredAt: event.occurredAt,
|
|
312
720
|
annotationSource: {
|
|
@@ -317,7 +725,10 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
317
725
|
turnId,
|
|
318
726
|
text,
|
|
319
727
|
},
|
|
320
|
-
}
|
|
728
|
+
};
|
|
729
|
+
items.push(item);
|
|
730
|
+
if (messageKey) identifiedMessages.set(messageKey, item);
|
|
731
|
+
rememberAgentResponse(turnId, item, true);
|
|
321
732
|
}
|
|
322
733
|
break;
|
|
323
734
|
}
|
|
@@ -346,9 +757,16 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
346
757
|
|
|
347
758
|
case "agent.toolCall.created": {
|
|
348
759
|
const name = typeof payload.name === "string" ? payload.name : "tool";
|
|
760
|
+
const display = parseToolDisplayMetadata(payload.display);
|
|
349
761
|
const callId = typeof payload.id === "string" ? payload.id : null;
|
|
350
762
|
const args = payload.arguments ?? null;
|
|
351
763
|
closeStreamingTail();
|
|
764
|
+
if (callId && humanInputToolCallIds.has(callId)) {
|
|
765
|
+
// The matching session.humanInput.requested / response lifecycle owns
|
|
766
|
+
// the visible chat card. Rendering the raw tool too would hide the
|
|
767
|
+
// decision behind a duplicate generic "Ask" step.
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
352
770
|
if (
|
|
353
771
|
toolMatchesLeaf(name, WORKER_SPAWN_TOOL) ||
|
|
354
772
|
toolMatchesLeaf(name, WORKER_MESSAGE_TOOL)
|
|
@@ -381,6 +799,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
381
799
|
(item): item is ToolCallItem => item.kind === "tool-call" && item.callId === callId,
|
|
382
800
|
);
|
|
383
801
|
if (existing) {
|
|
802
|
+
if (display) existing.display = display;
|
|
384
803
|
if (args != null) {
|
|
385
804
|
existing.arguments = args;
|
|
386
805
|
}
|
|
@@ -397,6 +816,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
397
816
|
turnId,
|
|
398
817
|
callId,
|
|
399
818
|
name,
|
|
819
|
+
...(display ? { display } : {}),
|
|
400
820
|
arguments: args,
|
|
401
821
|
output: undefined,
|
|
402
822
|
truncation: null,
|
|
@@ -415,12 +835,20 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
415
835
|
if (!target) {
|
|
416
836
|
break;
|
|
417
837
|
}
|
|
838
|
+
resolvePresentationWait(event, turnId ?? target.turnId);
|
|
418
839
|
if (target.kind === "worker") {
|
|
419
840
|
// A worker spawn/message that returns an error flag (or an MCP
|
|
420
841
|
// isError result) settles to "failed" too, so WorkerRow surfaces it.
|
|
421
842
|
target.status = isErrorOutput(payload) ? "failed" : "complete";
|
|
422
843
|
target.workerSessionId = target.workerSessionId ?? extractSessionRef(payload.output);
|
|
423
844
|
target.failure = target.status === "failed" ? workerFailure(payload.output) : null;
|
|
845
|
+
// A spawned or re-messaged worker owes this session a result.
|
|
846
|
+
if (target.status === "complete" && target.workerSessionId) {
|
|
847
|
+
if (target.action === "spawn") workerSessions.add(target.workerSessionId);
|
|
848
|
+
if (workerSessions.has(target.workerSessionId)) {
|
|
849
|
+
pendingWorkerSessions.add(target.workerSessionId);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
424
852
|
break;
|
|
425
853
|
}
|
|
426
854
|
// An output carrying an explicit error flag (or an MCP isError result)
|
|
@@ -452,15 +880,73 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
452
880
|
break;
|
|
453
881
|
}
|
|
454
882
|
|
|
883
|
+
case "turn.queued": {
|
|
884
|
+
const queuedTurnId = turnId ?? (typeof payload.turnId === "string" ? payload.turnId : null);
|
|
885
|
+
if (queuedTurnId) {
|
|
886
|
+
queuedAtByTurn.set(queuedTurnId, event.occurredAt);
|
|
887
|
+
}
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
case "turn.started": {
|
|
892
|
+
if (turnId) clearUnscopedTerminalTracking();
|
|
893
|
+
if (!turnId) break;
|
|
894
|
+
const queuedAt = queuedAtByTurn.get(turnId);
|
|
895
|
+
if (!queuedAt) break;
|
|
896
|
+
// Recovery starts the same logical turn again. Queue latency belongs to
|
|
897
|
+
// the first admission only, so consume the queued timestamp after its
|
|
898
|
+
// first projection instead of manufacturing another queue span.
|
|
899
|
+
queuedAtByTurn.delete(turnId);
|
|
900
|
+
closeStreamingTail();
|
|
901
|
+
settleStartupPhase(
|
|
902
|
+
"queue",
|
|
903
|
+
"complete",
|
|
904
|
+
elapsedDurationMs(queuedAt, event.occurredAt),
|
|
905
|
+
null,
|
|
906
|
+
0,
|
|
907
|
+
queuedAt,
|
|
908
|
+
`${event.id}-queue`,
|
|
909
|
+
);
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
|
|
455
913
|
case "sandbox.operation.started":
|
|
456
914
|
case "sandbox.operation.completed":
|
|
457
915
|
case "sandbox.operation.failed": {
|
|
458
916
|
const name = typeof payload.name === "string" ? payload.name : "sandbox";
|
|
459
917
|
const status = event.type.endsWith(".failed")
|
|
460
|
-
?
|
|
918
|
+
? payload.expectedTransition === true
|
|
919
|
+
? "cancelled"
|
|
920
|
+
: "failed"
|
|
461
921
|
: event.type.endsWith(".completed")
|
|
462
922
|
? "complete"
|
|
463
923
|
: "running";
|
|
924
|
+
const origin =
|
|
925
|
+
payload.origin === "created" ||
|
|
926
|
+
payload.origin === "restored" ||
|
|
927
|
+
payload.origin === "resumed"
|
|
928
|
+
? payload.origin
|
|
929
|
+
: null;
|
|
930
|
+
const startupPhase = startupPhaseForSandboxOperation(name);
|
|
931
|
+
if (startupPhase) {
|
|
932
|
+
closeStreamingTail();
|
|
933
|
+
settleStartupPhase(
|
|
934
|
+
startupPhase,
|
|
935
|
+
status,
|
|
936
|
+
numberOrNull(payload.durationMs),
|
|
937
|
+
origin,
|
|
938
|
+
0,
|
|
939
|
+
undefined,
|
|
940
|
+
undefined,
|
|
941
|
+
status === "cancelled"
|
|
942
|
+
? (startupLifecycleWaitReason(payload) ??
|
|
943
|
+
(payload.failureCode === "rotation_in_progress"
|
|
944
|
+
? "rotation_in_progress"
|
|
945
|
+
: undefined))
|
|
946
|
+
: undefined,
|
|
947
|
+
);
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
464
950
|
// Routine per-turn platform plumbing that runs before EVERY turn to
|
|
465
951
|
// guarantee box contents survive a re-warm — NOT the agent redoing work:
|
|
466
952
|
// - repository-clone: idempotent clone check + off-manifest token re-seed;
|
|
@@ -471,8 +957,13 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
471
957
|
// Rendering either every turn reads as churn. Only FAILURES surface, and
|
|
472
958
|
// they surface loudly — the failed event below creates its own item even
|
|
473
959
|
// without a started row.
|
|
960
|
+
// - optional-repository-access: the worker's per-turn report of an
|
|
961
|
+
// automatically attached repository that sat the turn out, reported
|
|
962
|
+
// like a skipped optional clone (event payload + log, no row).
|
|
474
963
|
if (
|
|
475
|
-
(name === "repository-clone" ||
|
|
964
|
+
(name === "repository-clone" ||
|
|
965
|
+
name === "file-resource-download" ||
|
|
966
|
+
name === "optional-repository-access") &&
|
|
476
967
|
status !== "failed"
|
|
477
968
|
) {
|
|
478
969
|
break;
|
|
@@ -480,13 +971,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
480
971
|
const existing = findOpenSandbox(items, name);
|
|
481
972
|
if (existing && status !== "running") {
|
|
482
973
|
existing.status = status;
|
|
483
|
-
if (
|
|
484
|
-
payload.origin === "created" ||
|
|
485
|
-
payload.origin === "restored" ||
|
|
486
|
-
payload.origin === "resumed"
|
|
487
|
-
) {
|
|
488
|
-
existing.origin = payload.origin;
|
|
489
|
-
}
|
|
974
|
+
if (origin) existing.origin = origin;
|
|
490
975
|
const message = failureMessage(payload);
|
|
491
976
|
if (message) {
|
|
492
977
|
existing.output = existing.output ? `${existing.output}\n${message}` : message;
|
|
@@ -502,12 +987,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
502
987
|
name,
|
|
503
988
|
command: typeof payload.command === "string" ? payload.command : null,
|
|
504
989
|
output: failureMessage(payload) ?? "",
|
|
505
|
-
origin
|
|
506
|
-
payload.origin === "created" ||
|
|
507
|
-
payload.origin === "restored" ||
|
|
508
|
-
payload.origin === "resumed"
|
|
509
|
-
? payload.origin
|
|
510
|
-
: null,
|
|
990
|
+
origin,
|
|
511
991
|
status,
|
|
512
992
|
occurredAt: event.occurredAt,
|
|
513
993
|
});
|
|
@@ -515,6 +995,29 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
515
995
|
break;
|
|
516
996
|
}
|
|
517
997
|
|
|
998
|
+
case "agent.model.request": {
|
|
999
|
+
const requestPhase = typeof payload.phase === "string" ? payload.phase : null;
|
|
1000
|
+
const status =
|
|
1001
|
+
requestPhase === "started"
|
|
1002
|
+
? "running"
|
|
1003
|
+
: requestPhase === "first_byte" || requestPhase === "first_event"
|
|
1004
|
+
? "complete"
|
|
1005
|
+
: requestPhase === "failed" || requestPhase === "timed_out"
|
|
1006
|
+
? "failed"
|
|
1007
|
+
: null;
|
|
1008
|
+
if (status) {
|
|
1009
|
+
if (status === "running") closeStreamingTail();
|
|
1010
|
+
settleStartupPhase(
|
|
1011
|
+
"provider_first_byte",
|
|
1012
|
+
status,
|
|
1013
|
+
status === "running" ? null : numberOrNull(payload.durationMs),
|
|
1014
|
+
null,
|
|
1015
|
+
status === "running" ? 2 : 1,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
518
1021
|
case "sandbox.command.output.delta": {
|
|
519
1022
|
// `chunk` is the canonical wire field; text/output are legacy shapes.
|
|
520
1023
|
const text =
|
|
@@ -548,6 +1051,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
548
1051
|
if (!isSessionStatus(status)) {
|
|
549
1052
|
break;
|
|
550
1053
|
}
|
|
1054
|
+
if (status === "running") resolvePresentationWait(event, turnId);
|
|
551
1055
|
// Only attention-worthy statuses earn a timeline divider. queued /
|
|
552
1056
|
// running / idle are machinery telemetry: the header pill carries the
|
|
553
1057
|
// live status, the shimmer says "running", and the turn chip's duration
|
|
@@ -559,30 +1063,57 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
559
1063
|
const previous = [...items]
|
|
560
1064
|
.reverse()
|
|
561
1065
|
.find((item): item is SessionStatusItem => item.kind === "session-status");
|
|
562
|
-
if (previous?.status === status) {
|
|
1066
|
+
if (previous?.status === status && !previous.resolvedAt) {
|
|
563
1067
|
break;
|
|
564
1068
|
}
|
|
565
|
-
|
|
1069
|
+
const item: SessionStatusItem = {
|
|
566
1070
|
kind: "session-status",
|
|
567
1071
|
id: event.id,
|
|
568
1072
|
status,
|
|
569
1073
|
occurredAt: event.occurredAt,
|
|
570
|
-
}
|
|
1074
|
+
};
|
|
1075
|
+
if (status === "requires_action") rememberPresentationWait(item, turnId);
|
|
1076
|
+
else {
|
|
1077
|
+
items.push(item);
|
|
1078
|
+
if (status === "failed") {
|
|
1079
|
+
const key = turnId ?? presentationTurnId;
|
|
1080
|
+
const failures = presentationFailures.get(key) ?? [];
|
|
1081
|
+
failures.push(item);
|
|
1082
|
+
presentationFailures.set(key, failures);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
571
1085
|
break;
|
|
572
1086
|
}
|
|
573
1087
|
|
|
574
|
-
case "
|
|
575
|
-
finalizeOpen(turnId);
|
|
1088
|
+
case "codex.capacity.waiting": {
|
|
576
1089
|
items.push({
|
|
577
1090
|
kind: "notice",
|
|
578
1091
|
id: event.id,
|
|
579
1092
|
tone: "waiting",
|
|
580
|
-
text:
|
|
1093
|
+
text:
|
|
1094
|
+
stringValue(payload.detail) ??
|
|
1095
|
+
stringValue(payload.error) ??
|
|
1096
|
+
"Waiting for Codex capacity.",
|
|
581
1097
|
occurredAt: event.occurredAt,
|
|
582
1098
|
});
|
|
583
1099
|
break;
|
|
584
1100
|
}
|
|
585
1101
|
|
|
1102
|
+
case "session.requiresAction": {
|
|
1103
|
+
finalizeOpen(turnId, "complete", event.occurredAt);
|
|
1104
|
+
rememberPresentationWait(
|
|
1105
|
+
{
|
|
1106
|
+
kind: "notice",
|
|
1107
|
+
id: event.id,
|
|
1108
|
+
tone: "waiting",
|
|
1109
|
+
text: "Approval needed — the turn is paused until someone decides.",
|
|
1110
|
+
occurredAt: event.occurredAt,
|
|
1111
|
+
},
|
|
1112
|
+
turnId,
|
|
1113
|
+
);
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
586
1117
|
case "session.context.compaction.requested":
|
|
587
1118
|
case "session.context.compaction.started": {
|
|
588
1119
|
closeStreamingTail();
|
|
@@ -594,6 +1125,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
594
1125
|
estimatedTokensBefore: numberOrNull(payload.estimatedTokensBefore),
|
|
595
1126
|
estimatedTokensAfter: null,
|
|
596
1127
|
skipReason: null,
|
|
1128
|
+
providerRejection: null,
|
|
597
1129
|
implementation:
|
|
598
1130
|
typeof payload.implementation === "string" ? payload.implementation : null,
|
|
599
1131
|
occurredAt: event.occurredAt,
|
|
@@ -611,6 +1143,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
611
1143
|
estimatedTokensBefore: numberOrNull(payload.estimatedTokensBefore),
|
|
612
1144
|
estimatedTokensAfter: numberOrNull(payload.estimatedTokensAfter),
|
|
613
1145
|
skipReason: null,
|
|
1146
|
+
providerRejection: null,
|
|
614
1147
|
implementation:
|
|
615
1148
|
typeof payload.implementation === "string" ? payload.implementation : null,
|
|
616
1149
|
occurredAt: event.occurredAt,
|
|
@@ -628,6 +1161,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
628
1161
|
estimatedTokensBefore: numberOrNull(payload.estimatedTokensBefore),
|
|
629
1162
|
estimatedTokensAfter: null,
|
|
630
1163
|
skipReason: typeof payload.reason === "string" ? payload.reason : null,
|
|
1164
|
+
providerRejection: compactionProviderRejection(payload),
|
|
631
1165
|
implementation:
|
|
632
1166
|
typeof payload.implementation === "string" ? payload.implementation : null,
|
|
633
1167
|
occurredAt: event.occurredAt,
|
|
@@ -639,6 +1173,29 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
639
1173
|
// Recovery requests are durable control-plane evidence, not a user
|
|
640
1174
|
// message or proof that recovery succeeded. Live state already exposes
|
|
641
1175
|
// a genuinely recovering session; keep the raw event in Debug/audit.
|
|
1176
|
+
if (turnId) {
|
|
1177
|
+
startupRecoveryRevisionByTurn.set(
|
|
1178
|
+
turnId,
|
|
1179
|
+
(startupRecoveryRevisionByTurn.get(turnId) ?? 0) + 1,
|
|
1180
|
+
);
|
|
1181
|
+
if (
|
|
1182
|
+
!event.duplicateOfEventId &&
|
|
1183
|
+
(!event.turnAssociation || event.turnAssociation === "current")
|
|
1184
|
+
) {
|
|
1185
|
+
// Human Retry reopens the existing logical turn, unlike a new
|
|
1186
|
+
// prompt. Keep failure evidence, but do not let its old settlement
|
|
1187
|
+
// close the recovered turn's work surface.
|
|
1188
|
+
for (const item of items) {
|
|
1189
|
+
if (item.kind === "turn-end" && item.turnId === turnId && item.outcome === "failed") {
|
|
1190
|
+
item.resumedAt ??= event.occurredAt;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
for (const item of presentationFailures.get(turnId) ?? []) {
|
|
1194
|
+
item.resolvedAt = event.occurredAt;
|
|
1195
|
+
}
|
|
1196
|
+
presentationFailures.delete(turnId);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
642
1199
|
break;
|
|
643
1200
|
}
|
|
644
1201
|
|
|
@@ -652,6 +1209,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
652
1209
|
case "tool.auth_needed":
|
|
653
1210
|
case "credential.auth_needed": {
|
|
654
1211
|
const capability = capabilityAuthorizationRequest(payload.capability);
|
|
1212
|
+
const setupRequest = customMcpSetupRequest(payload.setupRequest);
|
|
655
1213
|
if (
|
|
656
1214
|
event.type === "tool.auth_needed" &&
|
|
657
1215
|
!stringValue(payload.toolName) &&
|
|
@@ -674,25 +1232,44 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
674
1232
|
id: event.id,
|
|
675
1233
|
turnId,
|
|
676
1234
|
serverId: typeof payload.serverId === "string" ? payload.serverId : null,
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1235
|
+
canonicalServerId:
|
|
1236
|
+
typeof payload.canonicalServerId === "string" ? payload.canonicalServerId : null,
|
|
1237
|
+
connectionSubjectScope:
|
|
1238
|
+
payload.connectionSubjectScope === "subject" ||
|
|
1239
|
+
payload.connectionSubjectScope === "workspace"
|
|
1240
|
+
? payload.connectionSubjectScope
|
|
1241
|
+
: null,
|
|
1242
|
+
source:
|
|
1243
|
+
capability || setupRequest
|
|
1244
|
+
? "capability"
|
|
1245
|
+
: event.type === "tool.auth_needed"
|
|
1246
|
+
? "tool"
|
|
1247
|
+
: "credential",
|
|
682
1248
|
providerDomain: stringValue(payload.providerDomain),
|
|
683
1249
|
connectionId: typeof payload.connectionId === "string" ? payload.connectionId : null,
|
|
684
|
-
|
|
1250
|
+
authoritySource: payload.authoritySource === "host" ? "host" : null,
|
|
1251
|
+
reason: authNeededReason(
|
|
1252
|
+
payload.authoritySource === "host" && typeof payload.authorizationUrl === "string"
|
|
1253
|
+
? (payload.hostReason ?? payload.reason)
|
|
1254
|
+
: payload.reason,
|
|
1255
|
+
),
|
|
685
1256
|
scopes: stringList(payload.scopes),
|
|
686
1257
|
resource: typeof payload.resource === "string" ? payload.resource : null,
|
|
687
1258
|
toolName: typeof payload.toolName === "string" ? payload.toolName : null,
|
|
688
1259
|
authorizationUrl:
|
|
689
1260
|
typeof payload.authorizationUrl === "string" ? payload.authorizationUrl : null,
|
|
690
1261
|
capability,
|
|
1262
|
+
setupRequest,
|
|
691
1263
|
occurredAt: event.occurredAt,
|
|
692
1264
|
});
|
|
693
1265
|
break;
|
|
694
1266
|
}
|
|
695
1267
|
|
|
1268
|
+
case "usage.exhausted": {
|
|
1269
|
+
allowanceNotice(event, turnId);
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
696
1273
|
case "turn.completed": {
|
|
697
1274
|
// A standalone manual compaction uses the turn ledger for fencing and
|
|
698
1275
|
// recovery, but it is maintenance rather than a conversational turn.
|
|
@@ -700,7 +1277,9 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
700
1277
|
// adding a generic turn chip would falsely make it look like an
|
|
701
1278
|
// extra agent response.
|
|
702
1279
|
if (payload.maintenance === "context_compaction") {
|
|
703
|
-
finalizeOpen(turnId);
|
|
1280
|
+
finalizeOpen(turnId, "complete", event.occurredAt);
|
|
1281
|
+
takeAgentResponse(turnId);
|
|
1282
|
+
takePendingWaitOutcome(turnId);
|
|
704
1283
|
break;
|
|
705
1284
|
}
|
|
706
1285
|
// Credit exhaustion arrives as a NOMINALLY completed turn (`detail:
|
|
@@ -709,8 +1288,19 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
709
1288
|
// Rendering it as a clean "complete" turn is a lie that leaves the
|
|
710
1289
|
// session looking healthy while every future turn silently dies, so it
|
|
711
1290
|
// projects exactly like a failed turn plus an explicit notice.
|
|
1291
|
+
const allowance = parseAllowanceExhaustedRefusal(payload);
|
|
1292
|
+
if (allowance) {
|
|
1293
|
+
finalizeOpen(turnId, "complete", event.occurredAt);
|
|
1294
|
+
takeAgentResponse(turnId);
|
|
1295
|
+
takePendingWaitOutcome(turnId);
|
|
1296
|
+
items.push(turnEndItem(event, "failed", ALLOWANCE_TURN_END_TEXT));
|
|
1297
|
+
allowanceNotice(event, turnId);
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
712
1300
|
if (isCreditExhaustionPayload(payload)) {
|
|
713
|
-
finalizeOpen(turnId);
|
|
1301
|
+
finalizeOpen(turnId, "complete", event.occurredAt);
|
|
1302
|
+
takeAgentResponse(turnId);
|
|
1303
|
+
takePendingWaitOutcome(turnId);
|
|
714
1304
|
items.push(turnEndItem(event, "failed", CREDIT_EXHAUSTION_MESSAGE));
|
|
715
1305
|
items.push({
|
|
716
1306
|
kind: "notice",
|
|
@@ -721,26 +1311,116 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
721
1311
|
});
|
|
722
1312
|
break;
|
|
723
1313
|
}
|
|
724
|
-
|
|
725
|
-
|
|
1314
|
+
const latestAgentResponse = takeAgentResponse(turnId);
|
|
1315
|
+
const finalOutput = stringValue(payload.output);
|
|
1316
|
+
const visibleFinalOutput = stripOpaqueCitationTokens(finalOutput).trim();
|
|
1317
|
+
const visibleTrackedResponse = stripOpaqueCitationTokens(
|
|
1318
|
+
latestAgentResponse?.item.text ?? "",
|
|
1319
|
+
).trim();
|
|
1320
|
+
const finalOutputMirrorsCommentary =
|
|
1321
|
+
latestAgentResponse?.item.phase === "commentary" &&
|
|
1322
|
+
visibleTrackedResponse === visibleFinalOutput;
|
|
1323
|
+
const hasAuthoritativeFinalOutput =
|
|
1324
|
+
Boolean(visibleFinalOutput) && !finalOutputMirrorsCommentary;
|
|
1325
|
+
const pendingWaitOutcome = takePendingWaitOutcome(turnId);
|
|
1326
|
+
if (hasAuthoritativeFinalOutput) {
|
|
1327
|
+
// The final message normally lands as its own completion before
|
|
1328
|
+
// `turn.completed` (older workers commit a copy with it), but legacy or
|
|
1329
|
+
// partially compacted ledgers may retain only the terminal output
|
|
1330
|
+
// receipt. Keep that authoritative response visible instead of
|
|
1331
|
+
// leaving it trapped in raw audit data.
|
|
1332
|
+
if (
|
|
1333
|
+
latestAgentResponse &&
|
|
1334
|
+
!latestAgentResponse.completed &&
|
|
1335
|
+
finalOutput.startsWith(latestAgentResponse.item.text)
|
|
1336
|
+
) {
|
|
1337
|
+
latestAgentResponse.item.text = finalOutput;
|
|
1338
|
+
latestAgentResponse.item.streaming = false;
|
|
1339
|
+
if (
|
|
1340
|
+
latestAgentResponse.item.startedAt === undefined &&
|
|
1341
|
+
latestAgentResponse.item.occurredAt !== event.occurredAt
|
|
1342
|
+
) {
|
|
1343
|
+
latestAgentResponse.item.startedAt = latestAgentResponse.item.occurredAt;
|
|
1344
|
+
}
|
|
1345
|
+
latestAgentResponse.item.occurredAt = event.occurredAt;
|
|
1346
|
+
const responseIndex = items.indexOf(latestAgentResponse.item);
|
|
1347
|
+
if (responseIndex >= 0 && responseIndex < items.length - 1) {
|
|
1348
|
+
items.splice(responseIndex, 1);
|
|
1349
|
+
items.push(latestAgentResponse.item);
|
|
1350
|
+
}
|
|
1351
|
+
} else if (visibleTrackedResponse !== visibleFinalOutput) {
|
|
1352
|
+
items.push({
|
|
1353
|
+
kind: "agent-message",
|
|
1354
|
+
id: `${event.id}-output-message`,
|
|
1355
|
+
turnId,
|
|
1356
|
+
text: finalOutput,
|
|
1357
|
+
streaming: false,
|
|
1358
|
+
occurredAt: event.occurredAt,
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
finalizeOpen(turnId, "complete", event.occurredAt);
|
|
1363
|
+
const completedTurn = turnEndItem(event, "complete", null);
|
|
1364
|
+
items.push(completedTurn);
|
|
1365
|
+
if (turnCompletedWithEmptyFinalReply(payload)) {
|
|
1366
|
+
items.push({
|
|
1367
|
+
kind: "notice",
|
|
1368
|
+
id: `${event.id}-empty-final-reply`,
|
|
1369
|
+
tone: "input",
|
|
1370
|
+
text: EMPTY_FINAL_REPLY_NOTICE,
|
|
1371
|
+
recordedOutcome: true,
|
|
1372
|
+
occurredAt: event.occurredAt,
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
const hasCompletedFinalResponse =
|
|
1376
|
+
latestAgentResponse?.completed === true &&
|
|
1377
|
+
latestAgentResponse.item.phase !== "commentary" &&
|
|
1378
|
+
Boolean(visibleTrackedResponse);
|
|
1379
|
+
if (!hasAuthoritativeFinalOutput && pendingWaitOutcome) {
|
|
1380
|
+
// Preserve existing prose, including delta-only streams, when a
|
|
1381
|
+
// trailing wait yields without a terminal output receipt.
|
|
1382
|
+
completedTurn.preserveWaitResponse = true;
|
|
1383
|
+
}
|
|
1384
|
+
if (!hasAuthoritativeFinalOutput && !hasCompletedFinalResponse && pendingWaitOutcome) {
|
|
1385
|
+
const notice: NoticeItem = {
|
|
1386
|
+
kind: "notice",
|
|
1387
|
+
id: `${pendingWaitOutcome.id}-visible-outcome`,
|
|
1388
|
+
tone: "waiting",
|
|
1389
|
+
text: waitingOutcomeText(pendingWaitOutcome.reason),
|
|
1390
|
+
recordedOutcome: true,
|
|
1391
|
+
...(pendingWorkerSessions.size > 0
|
|
1392
|
+
? { waitingAgents: pendingWorkerSessions.size }
|
|
1393
|
+
: {}),
|
|
1394
|
+
occurredAt: pendingWaitOutcome.occurredAt,
|
|
1395
|
+
};
|
|
1396
|
+
items.push(notice);
|
|
1397
|
+
openWaitNotice = notice;
|
|
1398
|
+
}
|
|
726
1399
|
break;
|
|
727
1400
|
}
|
|
728
1401
|
|
|
729
1402
|
case "turn.failed": {
|
|
1403
|
+
takeAgentResponse(turnId);
|
|
1404
|
+
takePendingWaitOutcome(turnId);
|
|
730
1405
|
const hadActivity = hasTurnActivity(items, turnId);
|
|
731
1406
|
// Credit death can hide behind fields `failureMessage` doesn't read
|
|
732
1407
|
// (detail/segmentLimit), so classify the whole payload before falling
|
|
733
1408
|
// back to the generic error/message extraction.
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
1409
|
+
const allowance = parseAllowanceExhaustedRefusal(payload);
|
|
1410
|
+
const failureText = allowance
|
|
1411
|
+
? ALLOWANCE_TURN_END_TEXT
|
|
1412
|
+
: isCreditExhaustionPayload(payload)
|
|
1413
|
+
? CREDIT_EXHAUSTION_MESSAGE
|
|
1414
|
+
: failureMessage(payload);
|
|
737
1415
|
// The TURN failed — the in-flight items did not. Chip doctrine: red is
|
|
738
1416
|
// spent once, on the turn-level outcome. Items caught mid-flight read
|
|
739
1417
|
// as calm "interrupted" (same as turn.cancelled); an item that itself
|
|
740
1418
|
// failed keeps its own failed status from its output event.
|
|
741
|
-
finalizeOpen(turnId, "cancelled");
|
|
1419
|
+
finalizeOpen(turnId, "cancelled", event.occurredAt);
|
|
742
1420
|
items.push(turnEndItem(event, "failed", failureText));
|
|
743
|
-
if (
|
|
1421
|
+
if (allowance) {
|
|
1422
|
+
allowanceNotice(event, turnId);
|
|
1423
|
+
} else if (!hadActivity) {
|
|
744
1424
|
items.push({
|
|
745
1425
|
kind: "notice",
|
|
746
1426
|
id: event.id,
|
|
@@ -752,15 +1432,18 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
752
1432
|
break;
|
|
753
1433
|
}
|
|
754
1434
|
|
|
1435
|
+
case "turn.superseded":
|
|
755
1436
|
case "turn.cancelled": {
|
|
756
1437
|
// A retraction of a never-started queued turn is not a turn ending —
|
|
757
1438
|
// the message was withdrawn before any work happened; show nothing.
|
|
758
1439
|
// A null turnId proves nothing, so it keeps the legacy finalize path.
|
|
759
|
-
if (turnId && !prescan.startedTurnIds.has(turnId)) {
|
|
1440
|
+
if (event.type === "turn.cancelled" && turnId && !prescan.startedTurnIds.has(turnId)) {
|
|
760
1441
|
break;
|
|
761
1442
|
}
|
|
1443
|
+
takeAgentResponse(turnId);
|
|
1444
|
+
takePendingWaitOutcome(turnId);
|
|
762
1445
|
const hadActivity = hasTurnActivity(items, turnId);
|
|
763
|
-
finalizeOpen(turnId, "cancelled");
|
|
1446
|
+
finalizeOpen(turnId, "cancelled", event.occurredAt);
|
|
764
1447
|
items.push(turnEndItem(event, "cancelled", null));
|
|
765
1448
|
if (!hadActivity) {
|
|
766
1449
|
items.push({
|
|
@@ -774,6 +1457,45 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
774
1457
|
break;
|
|
775
1458
|
}
|
|
776
1459
|
|
|
1460
|
+
case "instruction.confirmation.recovered": {
|
|
1461
|
+
const receipt = asRecord(payload.receipt);
|
|
1462
|
+
if (receipt.outcome !== "published") break;
|
|
1463
|
+
closeStreamingTail();
|
|
1464
|
+
items.push({
|
|
1465
|
+
kind: "notice",
|
|
1466
|
+
id: event.id,
|
|
1467
|
+
tone: "input",
|
|
1468
|
+
text: "Your previously approved workspace instruction was saved.",
|
|
1469
|
+
occurredAt: event.occurredAt,
|
|
1470
|
+
});
|
|
1471
|
+
break;
|
|
1472
|
+
}
|
|
1473
|
+
case "knowledge.confirmation.recovered":
|
|
1474
|
+
case "knowledge.source.prepared":
|
|
1475
|
+
case "knowledge.source.failed": {
|
|
1476
|
+
const receipt = asRecord(payload.receipt);
|
|
1477
|
+
const outcome = event.type === "knowledge.source.failed" ? "failed" : receipt.outcome;
|
|
1478
|
+
if (
|
|
1479
|
+
(event.type !== "knowledge.confirmation.recovered" &&
|
|
1480
|
+
typeof payload.fileId !== "string") ||
|
|
1481
|
+
!["published", "pending", "rejected", "archived", "failed"].includes(String(outcome))
|
|
1482
|
+
)
|
|
1483
|
+
break;
|
|
1484
|
+
closeStreamingTail();
|
|
1485
|
+
items.push({
|
|
1486
|
+
kind: "knowledge",
|
|
1487
|
+
id: event.id,
|
|
1488
|
+
turnId,
|
|
1489
|
+
fileId: typeof payload.fileId === "string" ? payload.fileId : undefined,
|
|
1490
|
+
filename: typeof payload.filename === "string" ? payload.filename : undefined,
|
|
1491
|
+
entryId: typeof receipt.entryId === "string" ? receipt.entryId : undefined,
|
|
1492
|
+
status: outcome === "failed" ? "failed" : "complete",
|
|
1493
|
+
outcome: outcome as "published" | "pending" | "rejected" | "archived" | "failed",
|
|
1494
|
+
occurredAt: event.occurredAt,
|
|
1495
|
+
});
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
777
1499
|
case "memory.saved":
|
|
778
1500
|
case "memory.corrected": {
|
|
779
1501
|
// A first-party memory write is a discrete step, not streamed text, so it
|
|
@@ -802,7 +1524,11 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
802
1524
|
case "goal.paused":
|
|
803
1525
|
case "goal.resumed":
|
|
804
1526
|
case "goal.cleared":
|
|
1527
|
+
case "goal.held":
|
|
805
1528
|
case "goal.continuation": {
|
|
1529
|
+
if (event.type === "goal.held" && payload.actor === "agent") {
|
|
1530
|
+
rememberPendingWaitOutcome(pendingWaitOutcomeByTurn, event, payload, turnId);
|
|
1531
|
+
}
|
|
806
1532
|
// Agent tool mutations already appear as tool-call rows in the activity
|
|
807
1533
|
// cluster. Re-emitting them as GoalRow landmarks splits "N steps" mid-turn.
|
|
808
1534
|
if (shouldSuppressAgentGoalLandmark(event.type, payload)) {
|
|
@@ -812,18 +1538,37 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
812
1538
|
kind: "goal",
|
|
813
1539
|
id: event.id,
|
|
814
1540
|
action: event.type.slice("goal.".length) as GoalItem["action"],
|
|
815
|
-
text:
|
|
1541
|
+
text:
|
|
1542
|
+
event.type === "goal.held" && typeof payload.reason === "string" && payload.reason
|
|
1543
|
+
? payload.reason
|
|
1544
|
+
: goalText(payload),
|
|
816
1545
|
occurredAt: event.occurredAt,
|
|
817
1546
|
});
|
|
818
1547
|
break;
|
|
819
1548
|
}
|
|
820
1549
|
|
|
1550
|
+
case "session.wait.started": {
|
|
1551
|
+
if (payload.actor === "agent") {
|
|
1552
|
+
rememberPendingWaitOutcome(pendingWaitOutcomeByTurn, event, payload, turnId);
|
|
1553
|
+
}
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
821
1557
|
default:
|
|
822
1558
|
break;
|
|
823
1559
|
}
|
|
824
1560
|
}
|
|
825
1561
|
|
|
1562
|
+
const sourceEventsById = new Map(events.map((event) => [event.id, event]));
|
|
826
1563
|
for (const item of items) {
|
|
1564
|
+
const source = sourceEventsById.get(item.id);
|
|
1565
|
+
const canonical = "annotationSource" in item ? item.annotationSource : undefined;
|
|
1566
|
+
item.sourceEvents = [
|
|
1567
|
+
...(source ? [{ eventId: source.id, sequence: source.sequence }] : []),
|
|
1568
|
+
...(canonical && canonical.eventId !== source?.id
|
|
1569
|
+
? [{ eventId: canonical.eventId, sequence: canonical.sequence }]
|
|
1570
|
+
: []),
|
|
1571
|
+
];
|
|
827
1572
|
if (item.kind === "agent-message") {
|
|
828
1573
|
item.text = stripOpaqueCitationTokens(item.text);
|
|
829
1574
|
}
|
|
@@ -849,6 +1594,134 @@ function realtimeVoiceMessage(payload: Record<string, unknown>): {
|
|
|
849
1594
|
return null;
|
|
850
1595
|
}
|
|
851
1596
|
|
|
1597
|
+
function humanInputRequestsById(events: SessionEvent[]): Map<string, Record<string, unknown>> {
|
|
1598
|
+
const requests = new Map<string, Record<string, unknown>>();
|
|
1599
|
+
for (const event of events) {
|
|
1600
|
+
if (event.type !== "session.humanInput.requested") continue;
|
|
1601
|
+
const request = asRecord(asRecord(event.payload).request);
|
|
1602
|
+
const requestId = stringValue(request.id);
|
|
1603
|
+
if (requestId) requests.set(requestId, request);
|
|
1604
|
+
}
|
|
1605
|
+
return requests;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function humanInputItemFromResponse(
|
|
1609
|
+
event: SessionEvent,
|
|
1610
|
+
payload: Record<string, unknown>,
|
|
1611
|
+
requests: Map<string, Record<string, unknown>>,
|
|
1612
|
+
): HumanInputItem | null {
|
|
1613
|
+
const response = asRecord(payload.response);
|
|
1614
|
+
const outcome = response.outcome;
|
|
1615
|
+
if (
|
|
1616
|
+
outcome !== "answered" &&
|
|
1617
|
+
outcome !== "skipped" &&
|
|
1618
|
+
outcome !== "expired" &&
|
|
1619
|
+
outcome !== "cancelled"
|
|
1620
|
+
) {
|
|
1621
|
+
return null;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const requestId = stringValue(payload.requestId);
|
|
1625
|
+
const request = requests.get(requestId);
|
|
1626
|
+
const questions = humanInputQuestions(request?.questions);
|
|
1627
|
+
const normalizedResponse: HumanInputResponse =
|
|
1628
|
+
outcome === "answered"
|
|
1629
|
+
? {
|
|
1630
|
+
outcome,
|
|
1631
|
+
answers: Array.isArray(response.answers)
|
|
1632
|
+
? response.answers
|
|
1633
|
+
.map(asRecord)
|
|
1634
|
+
.map(humanInputAnswer)
|
|
1635
|
+
.filter((answer): answer is HumanInputAnswer => answer !== null)
|
|
1636
|
+
: [],
|
|
1637
|
+
}
|
|
1638
|
+
: { outcome };
|
|
1639
|
+
const questionsById = new Map(questions.map((question) => [question.id, question]));
|
|
1640
|
+
const answers =
|
|
1641
|
+
normalizedResponse.outcome === "answered"
|
|
1642
|
+
? normalizedResponse.answers.map((answer) =>
|
|
1643
|
+
humanInputAnswerSummary(answer, questionsById.get(answer.questionId)),
|
|
1644
|
+
)
|
|
1645
|
+
: [];
|
|
1646
|
+
return {
|
|
1647
|
+
kind: "human-input",
|
|
1648
|
+
id: event.id,
|
|
1649
|
+
turnId: event.turnId ?? null,
|
|
1650
|
+
requestId,
|
|
1651
|
+
questions,
|
|
1652
|
+
response: normalizedResponse,
|
|
1653
|
+
answers,
|
|
1654
|
+
occurredAt: event.occurredAt,
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function humanInputQuestions(value: unknown): HumanInputQuestion[] {
|
|
1659
|
+
if (!Array.isArray(value)) return [];
|
|
1660
|
+
return value.map(asRecord).flatMap((question) => {
|
|
1661
|
+
const id = stringValue(question.id);
|
|
1662
|
+
const prompt = stringValue(question.prompt);
|
|
1663
|
+
const kind = question.kind;
|
|
1664
|
+
if (
|
|
1665
|
+
!id ||
|
|
1666
|
+
!prompt ||
|
|
1667
|
+
(kind !== "text" && kind !== "single_select" && kind !== "multi_select")
|
|
1668
|
+
) {
|
|
1669
|
+
return [];
|
|
1670
|
+
}
|
|
1671
|
+
const options = Array.isArray(question.options)
|
|
1672
|
+
? question.options.map(asRecord).flatMap((option) => {
|
|
1673
|
+
const optionId = stringValue(option.id);
|
|
1674
|
+
const label = stringValue(option.label);
|
|
1675
|
+
return optionId && label
|
|
1676
|
+
? [{ id: optionId, label, description: stringValue(option.description) || null }]
|
|
1677
|
+
: [];
|
|
1678
|
+
})
|
|
1679
|
+
: [];
|
|
1680
|
+
return [
|
|
1681
|
+
{
|
|
1682
|
+
id,
|
|
1683
|
+
kind,
|
|
1684
|
+
prompt,
|
|
1685
|
+
label: stringValue(question.label) || null,
|
|
1686
|
+
helpText: stringValue(question.helpText) || null,
|
|
1687
|
+
options,
|
|
1688
|
+
required: question.required === true,
|
|
1689
|
+
allowOther: question.allowOther === true,
|
|
1690
|
+
},
|
|
1691
|
+
];
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function humanInputAnswer(value: Record<string, unknown>): HumanInputAnswer | null {
|
|
1696
|
+
const questionId = stringValue(value.questionId);
|
|
1697
|
+
if (!questionId) return null;
|
|
1698
|
+
return {
|
|
1699
|
+
questionId,
|
|
1700
|
+
values: Array.isArray(value.values)
|
|
1701
|
+
? value.values.filter((answer): answer is string => typeof answer === "string")
|
|
1702
|
+
: [],
|
|
1703
|
+
...(typeof value.other === "string" && value.other ? { other: value.other } : {}),
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function humanInputAnswerSummary(
|
|
1708
|
+
answer: HumanInputAnswer,
|
|
1709
|
+
question: HumanInputQuestion | undefined,
|
|
1710
|
+
): HumanInputAnswerSummary {
|
|
1711
|
+
const optionLabels = new Map(question?.options.map((option) => [option.id, option.label]) ?? []);
|
|
1712
|
+
const values = answer.values.map((value) => optionLabels.get(value) || value);
|
|
1713
|
+
if (answer.other) values.push(answer.other);
|
|
1714
|
+
const fallbackLabel = answer.questionId
|
|
1715
|
+
.replace(/[_-]+/g, " ")
|
|
1716
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
1717
|
+
return {
|
|
1718
|
+
questionId: answer.questionId,
|
|
1719
|
+
label: question?.label || question?.prompt || fallbackLabel,
|
|
1720
|
+
prompt: question?.prompt || fallbackLabel,
|
|
1721
|
+
values,
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
|
|
852
1725
|
function providerNativeToolStatus(rawValue: unknown): ToolCallItem["status"] {
|
|
853
1726
|
const raw = asRecord(rawValue);
|
|
854
1727
|
if (raw.type !== "hosted_tool_call") {
|
|
@@ -909,8 +1782,16 @@ export function stripOpaqueCitationTokens(text: string): string {
|
|
|
909
1782
|
return text.replace(/\s*cite(?:[^]+)+/gu, "");
|
|
910
1783
|
}
|
|
911
1784
|
|
|
1785
|
+
/**
|
|
1786
|
+
* The turn summary's short outcome for a usage ceiling. The adjacent notice row
|
|
1787
|
+
* carries who can raise it and when it resets (and is host-customizable), so
|
|
1788
|
+
* the summary line stays a neutral fact instead of repeating remedy prose.
|
|
1789
|
+
*/
|
|
1790
|
+
const ALLOWANCE_TURN_END_TEXT = "Usage limit reached";
|
|
1791
|
+
|
|
912
1792
|
/** The turn-end payload shape, as `isCreditExhaustion` wants it. */
|
|
913
1793
|
function isCreditExhaustionPayload(payload: Record<string, unknown>): boolean {
|
|
1794
|
+
if (payload.code === "allowance_exhausted") return false;
|
|
914
1795
|
return isCreditExhaustion({
|
|
915
1796
|
error: typeof payload.error === "string" ? payload.error : null,
|
|
916
1797
|
detail: typeof payload.detail === "string" ? payload.detail : null,
|
|
@@ -961,18 +1842,24 @@ export function sessionStatusFromEvents(events: SessionEvent[]): SessionStatus |
|
|
|
961
1842
|
non-user span folds behind a turn group, with activity blocks nested inside.
|
|
962
1843
|
-------------------------------------------------------------------------- */
|
|
963
1844
|
|
|
1845
|
+
/** Work items: every activity kind except assistant commentary. */
|
|
1846
|
+
type WorkActivityItem = Exclude<ActivityItem, AgentMessageItem>;
|
|
1847
|
+
|
|
964
1848
|
/**
|
|
965
1849
|
* Whether an item clusters into an activity block. A `switch` (not a stringly-
|
|
966
1850
|
* typed set) so adding an {@link ActivityItem} kind is a compile-time prompt to
|
|
967
|
-
* decide its grouping
|
|
1851
|
+
* decide its grouping, and it narrows `item` to a work item with no cast.
|
|
1852
|
+
* Assistant prose stays outside activity clusters.
|
|
968
1853
|
*/
|
|
969
|
-
function isActivityItem(item: TimelineItem): item is
|
|
1854
|
+
function isActivityItem(item: TimelineItem): item is WorkActivityItem {
|
|
970
1855
|
switch (item.kind) {
|
|
971
1856
|
case "reasoning":
|
|
972
1857
|
case "tool-call":
|
|
973
1858
|
case "worker":
|
|
974
1859
|
case "sandbox":
|
|
1860
|
+
case "startup-phase":
|
|
975
1861
|
case "memory":
|
|
1862
|
+
case "knowledge":
|
|
976
1863
|
case "fleet-decision":
|
|
977
1864
|
return true;
|
|
978
1865
|
default:
|
|
@@ -980,7 +1867,18 @@ function isActivityItem(item: TimelineItem): item is ActivityItem {
|
|
|
980
1867
|
}
|
|
981
1868
|
}
|
|
982
1869
|
|
|
983
|
-
export
|
|
1870
|
+
export type GroupTimelineOptions = {
|
|
1871
|
+
/** Readable assistant prose with an independent work summary for each turn. */
|
|
1872
|
+
readableTurns?: boolean;
|
|
1873
|
+
/** @deprecated Use readableTurns. Cross-turn exchange folding has been removed. */
|
|
1874
|
+
foldExchanges?: boolean;
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
export function groupTimeline(
|
|
1878
|
+
items: TimelineItem[],
|
|
1879
|
+
options: GroupTimelineOptions = {},
|
|
1880
|
+
): TimelineGroup[] {
|
|
1881
|
+
if (options.readableTurns || options.foldExchanges) return groupReadableTurns(items);
|
|
984
1882
|
const groups: TimelineGroup[] = [];
|
|
985
1883
|
for (const item of items) {
|
|
986
1884
|
if (isActivityItem(item)) {
|
|
@@ -1006,32 +1904,429 @@ export function groupTimeline(items: TimelineItem[]): TimelineGroup[] {
|
|
|
1006
1904
|
return groups;
|
|
1007
1905
|
}
|
|
1008
1906
|
|
|
1907
|
+
/** Live prose stays primary; settled progress joins its own turn's work history. */
|
|
1908
|
+
function groupReadableTurns(items: TimelineItem[]): TimelineGroup[] {
|
|
1909
|
+
type Work = Extract<TimelineGroup, { kind: "activity" }>;
|
|
1910
|
+
let groups: TimelineGroup[] = [];
|
|
1911
|
+
const turns = new Map<string, Work>();
|
|
1912
|
+
// Turn identity and settlement exist even when a final-only turn has no
|
|
1913
|
+
// disclosure. A late receipt must not become a new turn or revive its clock.
|
|
1914
|
+
const seenTurns = new Set<string>();
|
|
1915
|
+
const settlements = new Map<
|
|
1916
|
+
string,
|
|
1917
|
+
{ endedAt: string; outcome?: Work["outcome"]; failureText?: string }
|
|
1918
|
+
>();
|
|
1919
|
+
const compactionTurns = new Map<ContextCompactionItem, string>();
|
|
1920
|
+
const messages = new Map<string, AgentMessageItem[]>();
|
|
1921
|
+
const inputs = new Map<string, Extract<TimelineItem, { kind: "machine-input-batch" }>>();
|
|
1922
|
+
const itemOrder = new Map(items.map((item, index) => [item.id, index]));
|
|
1923
|
+
let legacyTurn = "start";
|
|
1924
|
+
let currentTurn = legacyTurn;
|
|
1925
|
+
for (const item of items) {
|
|
1926
|
+
if (item.kind === "user-message") legacyTurn = item.id;
|
|
1927
|
+
const key = ("turnId" in item && item.turnId) || legacyTurn;
|
|
1928
|
+
// Legacy work still establishes a boundary using its prompt key.
|
|
1929
|
+
// A human message alone does not: it may be steering the existing turn.
|
|
1930
|
+
if (
|
|
1931
|
+
("turnId" in item && item.turnId) ||
|
|
1932
|
+
isActivityItem(item) ||
|
|
1933
|
+
(item.kind === "agent-message" && item.text.trim())
|
|
1934
|
+
) {
|
|
1935
|
+
if (key !== currentTurn && !seenTurns.has(key)) {
|
|
1936
|
+
const previous = turns.get(currentTurn);
|
|
1937
|
+
if (!settlements.has(currentTurn)) {
|
|
1938
|
+
settlements.set(currentTurn, { endedAt: item.occurredAt });
|
|
1939
|
+
}
|
|
1940
|
+
if (previous?.work && !previous.work.endedAt) previous.work.endedAt = item.occurredAt;
|
|
1941
|
+
currentTurn = key;
|
|
1942
|
+
}
|
|
1943
|
+
seenTurns.add(key);
|
|
1944
|
+
}
|
|
1945
|
+
const workForTurn = () => {
|
|
1946
|
+
let group = turns.get(key);
|
|
1947
|
+
const firstMessage = messages.get(key)?.[0];
|
|
1948
|
+
const messageStart = firstMessage?.startedAt ?? firstMessage?.occurredAt;
|
|
1949
|
+
const startedAt =
|
|
1950
|
+
messageStart && Date.parse(messageStart) < Date.parse(item.occurredAt)
|
|
1951
|
+
? messageStart
|
|
1952
|
+
: item.occurredAt;
|
|
1953
|
+
if (!group) {
|
|
1954
|
+
const settlement = settlements.get(key);
|
|
1955
|
+
group = {
|
|
1956
|
+
kind: "activity",
|
|
1957
|
+
id: `work-${key}`,
|
|
1958
|
+
items: [],
|
|
1959
|
+
work: { startedAt, details: [] },
|
|
1960
|
+
};
|
|
1961
|
+
if (settlement) {
|
|
1962
|
+
group.work!.endedAt = settlement.endedAt;
|
|
1963
|
+
if (settlement.outcome) group.outcome = settlement.outcome;
|
|
1964
|
+
if (settlement.failureText) group.failureText = settlement.failureText;
|
|
1965
|
+
}
|
|
1966
|
+
turns.set(key, group);
|
|
1967
|
+
const firstMessageIndex =
|
|
1968
|
+
settlement && firstMessage
|
|
1969
|
+
? groups.findIndex((entry) => entry.kind === "item" && entry.item === firstMessage)
|
|
1970
|
+
: -1;
|
|
1971
|
+
if (firstMessageIndex >= 0) groups.splice(firstMessageIndex, 0, group);
|
|
1972
|
+
else groups.push(group);
|
|
1973
|
+
} else if (Date.parse(startedAt) < Date.parse(group.work!.startedAt)) {
|
|
1974
|
+
// Completion-only startup receipts can reveal an earlier phase start
|
|
1975
|
+
// after another activity has already established the work row.
|
|
1976
|
+
group.work!.startedAt = startedAt;
|
|
1977
|
+
}
|
|
1978
|
+
return group;
|
|
1979
|
+
};
|
|
1980
|
+
if (isActivityItem(item)) {
|
|
1981
|
+
const group = workForTurn();
|
|
1982
|
+
group.items.push(item);
|
|
1983
|
+
const previous = group.work!.details.at(-1);
|
|
1984
|
+
const preparing = (entry: ActivityItem) =>
|
|
1985
|
+
entry.kind === "startup-phase" || (entry.kind === "reasoning" && !entry.text.trim());
|
|
1986
|
+
if (
|
|
1987
|
+
preparing(item) &&
|
|
1988
|
+
previous?.kind === "activity" &&
|
|
1989
|
+
previous.items.every(preparing) &&
|
|
1990
|
+
previous.items.at(-1) === items[(itemOrder.get(item.id) ?? 0) - 1]
|
|
1991
|
+
)
|
|
1992
|
+
previous.items.push(item);
|
|
1993
|
+
else
|
|
1994
|
+
group.work!.details.push({ kind: "activity", id: `work-item-${item.id}`, items: [item] });
|
|
1995
|
+
delete group.work!.waiting;
|
|
1996
|
+
} else if (item.kind === "turn-end") {
|
|
1997
|
+
if (!item.resumedAt) {
|
|
1998
|
+
settlements.set(key, {
|
|
1999
|
+
endedAt: item.occurredAt,
|
|
2000
|
+
outcome: item.outcome,
|
|
2001
|
+
...(item.failureText ? { failureText: item.failureText } : {}),
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
const group = turns.get(key);
|
|
2005
|
+
if (group?.work) {
|
|
2006
|
+
if (item.resumedAt) {
|
|
2007
|
+
group.work.details.push({
|
|
2008
|
+
kind: "item",
|
|
2009
|
+
item: {
|
|
2010
|
+
kind: "notice",
|
|
2011
|
+
id: item.id,
|
|
2012
|
+
tone: "failed",
|
|
2013
|
+
text: item.failureText ?? "The turn failed before retrying.",
|
|
2014
|
+
occurredAt: item.occurredAt,
|
|
2015
|
+
},
|
|
2016
|
+
});
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
group.outcome = item.outcome;
|
|
2020
|
+
if (item.failureText) group.failureText = item.failureText;
|
|
2021
|
+
group.work.endedAt = item.occurredAt;
|
|
2022
|
+
}
|
|
2023
|
+
} else if (item.kind === "context-compaction" && item.phase === "compacted") {
|
|
2024
|
+
// A maintenance checkpoint alone is not conversational work. Resolve its
|
|
2025
|
+
// attachment after real activity has established this turn's work row.
|
|
2026
|
+
compactionTurns.set(item, key);
|
|
2027
|
+
groups.push({ kind: "item", item });
|
|
2028
|
+
} else if (
|
|
2029
|
+
item.kind === "machine-input-batch" &&
|
|
2030
|
+
item.members.every((member) => ROUTINE_MACHINE_INPUT_KINDS.has(member.kind))
|
|
2031
|
+
) {
|
|
2032
|
+
const previous = inputs.get(key);
|
|
2033
|
+
if (previous) {
|
|
2034
|
+
previous.members.push(
|
|
2035
|
+
...item.members.filter((member) => !previous.members.some((old) => old.id === member.id)),
|
|
2036
|
+
);
|
|
2037
|
+
previous.sourceEvents = [...(previous.sourceEvents ?? []), ...(item.sourceEvents ?? [])];
|
|
2038
|
+
} else {
|
|
2039
|
+
const batch = { ...item, compact: true, members: [...item.members] };
|
|
2040
|
+
inputs.set(key, batch);
|
|
2041
|
+
groups.push({ kind: "item", item: batch });
|
|
2042
|
+
}
|
|
2043
|
+
} else if (
|
|
2044
|
+
item.kind === "worker-completion" ||
|
|
2045
|
+
(item.kind === "goal" && item.action === "continuation")
|
|
2046
|
+
) {
|
|
2047
|
+
const group = turns.get(currentTurn);
|
|
2048
|
+
if (group?.work) group.work.details.push({ kind: "item", item });
|
|
2049
|
+
else groups.push({ kind: "item", item });
|
|
2050
|
+
} else {
|
|
2051
|
+
const current = turns.get(currentTurn)?.work;
|
|
2052
|
+
// A live approval wait is carried by the turn header ("Waiting for you")
|
|
2053
|
+
// and decided in the host's approval surface; a second in-timeline banner
|
|
2054
|
+
// only repeats it. Once resolved it returns as the quiet recorded marker.
|
|
2055
|
+
const liveApprovalWait =
|
|
2056
|
+
item.kind === "notice" &&
|
|
2057
|
+
item.tone === "waiting" &&
|
|
2058
|
+
!item.recordedOutcome &&
|
|
2059
|
+
!item.resolvedAt &&
|
|
2060
|
+
item.text.startsWith("Approval needed") &&
|
|
2061
|
+
current !== undefined &&
|
|
2062
|
+
!current.endedAt;
|
|
2063
|
+
// Likewise the live "waiting on you" divider: the open turn's header
|
|
2064
|
+
// already reads "Waiting for you · <elapsed>". It returns, resolved, as
|
|
2065
|
+
// the "work resumed" divider once the session runs again.
|
|
2066
|
+
const liveStatusWait =
|
|
2067
|
+
item.kind === "session-status" &&
|
|
2068
|
+
item.status === "requires_action" &&
|
|
2069
|
+
!item.resolvedAt &&
|
|
2070
|
+
current !== undefined &&
|
|
2071
|
+
!current.endedAt;
|
|
2072
|
+
if (!liveApprovalWait && !liveStatusWait) groups.push({ kind: "item", item });
|
|
2073
|
+
if (current && !current.endedAt) {
|
|
2074
|
+
if (
|
|
2075
|
+
item.kind === "notice" &&
|
|
2076
|
+
item.tone === "waiting" &&
|
|
2077
|
+
!item.recordedOutcome &&
|
|
2078
|
+
!item.resolvedAt
|
|
2079
|
+
) {
|
|
2080
|
+
current.waiting = {
|
|
2081
|
+
label: item.text.startsWith("Approval needed") ? "Waiting for you" : "Waiting",
|
|
2082
|
+
since: item.occurredAt,
|
|
2083
|
+
};
|
|
2084
|
+
} else if (
|
|
2085
|
+
item.kind === "session-status" &&
|
|
2086
|
+
item.status === "requires_action" &&
|
|
2087
|
+
!item.resolvedAt
|
|
2088
|
+
) {
|
|
2089
|
+
current.waiting = { label: "Waiting for you", since: item.occurredAt };
|
|
2090
|
+
} else if (
|
|
2091
|
+
item.kind === "session-status" &&
|
|
2092
|
+
["paused", "cancelled", "failed"].includes(item.status) &&
|
|
2093
|
+
!item.resolvedAt
|
|
2094
|
+
) {
|
|
2095
|
+
current.endedAt = item.occurredAt;
|
|
2096
|
+
delete current.waiting;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
if (item.kind === "agent-message" && item.text.trim()) {
|
|
2100
|
+
const prose = messages.get(key) ?? [];
|
|
2101
|
+
prose.push(item);
|
|
2102
|
+
messages.set(key, prose);
|
|
2103
|
+
// A text-only turn still has one overall activity surface. An explicit
|
|
2104
|
+
// final-only response does not manufacture an empty work disclosure.
|
|
2105
|
+
if (
|
|
2106
|
+
(item.phase !== "final_answer" || prose.length > 1) &&
|
|
2107
|
+
(turns.has(key) || !settlements.has(key))
|
|
2108
|
+
)
|
|
2109
|
+
workForTurn();
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
groups = groups.filter((entry) => {
|
|
2114
|
+
if (entry.kind !== "item" || entry.item.kind !== "context-compaction") return true;
|
|
2115
|
+
const key = compactionTurns.get(entry.item);
|
|
2116
|
+
const work = key === undefined ? undefined : turns.get(key)?.work;
|
|
2117
|
+
if (!work) return true;
|
|
2118
|
+
work.details.push(entry);
|
|
2119
|
+
return false;
|
|
2120
|
+
});
|
|
2121
|
+
// A declared answer starts the response clock immediately. Without a phase,
|
|
2122
|
+
// only settlement identifies the last response; text length never does.
|
|
2123
|
+
const beforeRows = new Map<TimelineGroup, Work>();
|
|
2124
|
+
const afterRows = new Map<TimelineGroup, Work>();
|
|
2125
|
+
const movedRows = new Set<Work>();
|
|
2126
|
+
const foldedProse = new Set<AgentMessageItem>();
|
|
2127
|
+
const positions = new Map<TimelineGroup | TimelineItem, number>();
|
|
2128
|
+
const nextBoundary = new Map<Work, number>();
|
|
2129
|
+
let boundary = groups.length;
|
|
2130
|
+
for (let index = groups.length - 1; index >= 0; index -= 1) {
|
|
2131
|
+
const entry = groups[index]!;
|
|
2132
|
+
positions.set(entry.kind === "item" ? entry.item : entry, index);
|
|
2133
|
+
if (entry.kind === "activity" && entry.work) {
|
|
2134
|
+
nextBoundary.set(entry, boundary);
|
|
2135
|
+
boundary = index;
|
|
2136
|
+
} else if (
|
|
2137
|
+
entry.kind === "item" &&
|
|
2138
|
+
(entry.item.kind === "user-message" || entry.item.kind === "machine-input-batch")
|
|
2139
|
+
) {
|
|
2140
|
+
boundary = index;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
for (const [key, group] of turns) {
|
|
2144
|
+
const prose = messages.get(key) ?? [];
|
|
2145
|
+
const settledAt = group.work!.endedAt;
|
|
2146
|
+
const responseCandidates = settledAt
|
|
2147
|
+
? prose.filter(
|
|
2148
|
+
(message) => Date.parse(message.startedAt ?? message.occurredAt) <= Date.parse(settledAt),
|
|
2149
|
+
)
|
|
2150
|
+
: prose;
|
|
2151
|
+
const response =
|
|
2152
|
+
responseCandidates.filter((message) => message.phase === "final_answer").at(-1) ??
|
|
2153
|
+
(settledAt
|
|
2154
|
+
? (responseCandidates.at(-1) ??
|
|
2155
|
+
prose.filter((message) => message.phase === "final_answer").at(-1))
|
|
2156
|
+
: undefined);
|
|
2157
|
+
if (settledAt) {
|
|
2158
|
+
for (const message of responseCandidates) {
|
|
2159
|
+
// Markdown image syntax also carries retained video/audio previews.
|
|
2160
|
+
// Keep potential primary media visible rather than guessing whether a
|
|
2161
|
+
// partial/reference-style embed can safely disappear into history.
|
|
2162
|
+
if (message === response || message.text.includes("![")) continue;
|
|
2163
|
+
foldedProse.add(message);
|
|
2164
|
+
group.work!.details.push({ kind: "item", item: message });
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
// Preserve event chronology, not completion timestamps or activity kinds.
|
|
2168
|
+
group.work!.details.sort((a, b) => {
|
|
2169
|
+
const first = (entry: TimelineGroup) =>
|
|
2170
|
+
entry.kind === "item"
|
|
2171
|
+
? entry.item.id
|
|
2172
|
+
: entry.kind === "activity"
|
|
2173
|
+
? entry.items[0]?.id
|
|
2174
|
+
: undefined;
|
|
2175
|
+
return (itemOrder.get(first(a) ?? "") ?? 0) - (itemOrder.get(first(b) ?? "") ?? 0);
|
|
2176
|
+
});
|
|
2177
|
+
if (response) {
|
|
2178
|
+
const responseAt = response.startedAt ?? response.occurredAt;
|
|
2179
|
+
// A delayed completion-only receipt cannot extend a settled work clock.
|
|
2180
|
+
group.work!.responseStartedAt =
|
|
2181
|
+
settledAt && Date.parse(responseAt) > Date.parse(settledAt) ? settledAt : responseAt;
|
|
2182
|
+
// A late completion may belong to an older turn. Keep its work at its
|
|
2183
|
+
// original boundary rather than moving it across a newer turn's input.
|
|
2184
|
+
if (
|
|
2185
|
+
settledAt &&
|
|
2186
|
+
(positions.get(response) ?? groups.length) < (nextBoundary.get(group) ?? groups.length)
|
|
2187
|
+
) {
|
|
2188
|
+
beforeRows.set(groups[positions.get(response)!]!, group);
|
|
2189
|
+
movedRows.add(group);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
if (!settledAt) {
|
|
2193
|
+
// Keep the same row at the tail of this turn, after every live progress
|
|
2194
|
+
// message and attention surface, without crossing a newer input/turn.
|
|
2195
|
+
// A visible steer is not a turn boundary when later prose still belongs
|
|
2196
|
+
// to this turn. New input without that evidence remains a hard boundary.
|
|
2197
|
+
const lastProseIndex = Math.max(...prose.map((message) => positions.get(message) ?? -1));
|
|
2198
|
+
let tailIndex = groups.length - 1;
|
|
2199
|
+
for (let index = (positions.get(group) ?? 0) + 1; index <= tailIndex; index += 1) {
|
|
2200
|
+
const entry = groups[index]!;
|
|
2201
|
+
if (
|
|
2202
|
+
(entry.kind === "activity" && entry.work) ||
|
|
2203
|
+
(entry.kind === "item" &&
|
|
2204
|
+
(("turnId" in entry.item && entry.item.turnId && entry.item.turnId !== key) ||
|
|
2205
|
+
(index > lastProseIndex &&
|
|
2206
|
+
(entry.item.kind === "user-message" || entry.item.kind === "machine-input-batch"))))
|
|
2207
|
+
) {
|
|
2208
|
+
tailIndex = index - 1;
|
|
2209
|
+
break;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
const tail = groups[tailIndex];
|
|
2213
|
+
if (tail && tail !== group) {
|
|
2214
|
+
afterRows.set(tail, group);
|
|
2215
|
+
movedRows.add(group);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
return groups.flatMap((group) => {
|
|
2220
|
+
if (group.kind === "activity" && movedRows.has(group)) return [];
|
|
2221
|
+
const hidden =
|
|
2222
|
+
group.kind === "item" && group.item.kind === "agent-message" && foldedProse.has(group.item);
|
|
2223
|
+
const before = beforeRows.get(group);
|
|
2224
|
+
const after = afterRows.get(group);
|
|
2225
|
+
return [...(before ? [before] : []), ...(hidden ? [] : [group]), ...(after ? [after] : [])];
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
/** Machine inputs that continue the current exchange rather than start one. */
|
|
2230
|
+
const ROUTINE_MACHINE_INPUT_KINDS: ReadonlySet<MachineInputMember["kind"]> = new Set([
|
|
2231
|
+
"goal_continuation",
|
|
2232
|
+
"background_command_result",
|
|
2233
|
+
"session_wait_timeout",
|
|
2234
|
+
"agent_message",
|
|
2235
|
+
"agent_steer_instruction",
|
|
2236
|
+
"child_terminal_result",
|
|
2237
|
+
"child_requires_action_resolved",
|
|
2238
|
+
"child_paused",
|
|
2239
|
+
"child_waiting_capacity",
|
|
2240
|
+
"child_progress",
|
|
2241
|
+
]);
|
|
2242
|
+
|
|
1009
2243
|
/* --- helpers ---------------------------------------------------------------- */
|
|
1010
2244
|
|
|
1011
2245
|
type TurnAnchorPrescan = {
|
|
1012
2246
|
queuedTurnByTrigger: Map<string, string>;
|
|
1013
2247
|
startSeqByTrigger: Map<string, number>;
|
|
1014
2248
|
cancelledBeforeStartTriggers: Set<string>;
|
|
2249
|
+
directChatSequenceByTrigger: Map<string, number>;
|
|
2250
|
+
explicitQueuedTriggers: Set<string>;
|
|
1015
2251
|
startedTurnIds: Set<string>;
|
|
1016
2252
|
};
|
|
1017
2253
|
|
|
2254
|
+
/** Navigation uses the same queue/start/withdrawal rules as transcript ordering. */
|
|
2255
|
+
export function timelineQuestionPlacement(question: SessionEvent, evidence: SessionEvent[]) {
|
|
2256
|
+
const anchors = prescanTurnAnchors([question, ...evidence]);
|
|
2257
|
+
const direct = anchors.directChatSequenceByTrigger.get(question.id);
|
|
2258
|
+
if (direct !== undefined) return { kind: "visible" as const, sequence: direct };
|
|
2259
|
+
if (anchors.cancelledBeforeStartTriggers.has(question.id)) return { kind: "withdrawn" as const };
|
|
2260
|
+
const start = anchors.startSeqByTrigger.get(question.id);
|
|
2261
|
+
if (start !== undefined) return { kind: "visible" as const, sequence: start };
|
|
2262
|
+
if (
|
|
2263
|
+
anchors.queuedTurnByTrigger.has(question.id) ||
|
|
2264
|
+
anchors.explicitQueuedTriggers.has(question.id)
|
|
2265
|
+
)
|
|
2266
|
+
return { kind: "pending" as const };
|
|
2267
|
+
return { kind: "visible" as const, sequence: question.sequence };
|
|
2268
|
+
}
|
|
2269
|
+
|
|
1018
2270
|
function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
|
|
1019
2271
|
const ordered = [...events].sort((a, b) => a.sequence - b.sequence);
|
|
1020
2272
|
const queuedTurnByTrigger = new Map<string, string>();
|
|
1021
2273
|
const startSeqByTrigger = new Map<string, number>();
|
|
1022
2274
|
const cancelledTurnIds = new Set<string>();
|
|
2275
|
+
const withdrawnTurnIds = new Set<string>();
|
|
1023
2276
|
const fallbackSeqByTurn = new Map<string, number>();
|
|
2277
|
+
const directChatSequenceByTrigger = new Map<string, number>();
|
|
2278
|
+
const explicitQueuedTriggers = new Set<string>();
|
|
2279
|
+
const queueSteerSequenceByTurn = new Map<string, number>();
|
|
1024
2280
|
const startedTurnIds = new Set<string>();
|
|
2281
|
+
const userMessageSequenceById = new Map<string, number>();
|
|
1025
2282
|
|
|
1026
2283
|
for (const event of ordered) {
|
|
1027
2284
|
const payload = asRecord(event.payload);
|
|
1028
2285
|
const turnId = event.turnId ?? null;
|
|
2286
|
+
if (event.type === "user.message") {
|
|
2287
|
+
userMessageSequenceById.set(event.id, event.sequence);
|
|
2288
|
+
if (payload.routing === "queued_for_execution") {
|
|
2289
|
+
explicitQueuedTriggers.add(event.id);
|
|
2290
|
+
} else if (
|
|
2291
|
+
payload.routing === "accepted_for_execution" ||
|
|
2292
|
+
payload.routing === "accepted_for_steering"
|
|
2293
|
+
) {
|
|
2294
|
+
directChatSequenceByTrigger.set(event.id, event.sequence);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
if (event.type === "user.message" && payload.delivery === "steer") {
|
|
2298
|
+
directChatSequenceByTrigger.set(event.id, event.sequence);
|
|
2299
|
+
}
|
|
2300
|
+
if (event.type === "session.control.steer_requested") {
|
|
2301
|
+
const targetTurnId = typeof payload.targetTurnId === "string" ? payload.targetTurnId : null;
|
|
2302
|
+
if (targetTurnId && !queueSteerSequenceByTurn.has(targetTurnId)) {
|
|
2303
|
+
queueSteerSequenceByTurn.set(targetTurnId, event.sequence);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
if (
|
|
2307
|
+
event.type === "session.queue.changed" &&
|
|
2308
|
+
(payload.operation === "edit" || payload.operation === "delete")
|
|
2309
|
+
) {
|
|
2310
|
+
const withdrawnTurnId =
|
|
2311
|
+
typeof payload.turnId === "string" ? payload.turnId : (turnId ?? null);
|
|
2312
|
+
if (withdrawnTurnId) withdrawnTurnIds.add(withdrawnTurnId);
|
|
2313
|
+
}
|
|
1029
2314
|
if (event.type === "turn.queued") {
|
|
1030
2315
|
const triggerEventId =
|
|
1031
2316
|
typeof payload.triggerEventId === "string" ? payload.triggerEventId : null;
|
|
1032
2317
|
const queuedTurnId = typeof payload.turnId === "string" ? payload.turnId : turnId;
|
|
1033
2318
|
if (triggerEventId && queuedTurnId) {
|
|
1034
2319
|
queuedTurnByTrigger.set(triggerEventId, queuedTurnId);
|
|
2320
|
+
if (
|
|
2321
|
+
(payload.routing === "accepted_for_execution" ||
|
|
2322
|
+
payload.routing === "accepted_for_steering") &&
|
|
2323
|
+
!directChatSequenceByTrigger.has(triggerEventId)
|
|
2324
|
+
) {
|
|
2325
|
+
directChatSequenceByTrigger.set(
|
|
2326
|
+
triggerEventId,
|
|
2327
|
+
userMessageSequenceById.get(triggerEventId) ?? event.sequence,
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
1035
2330
|
}
|
|
1036
2331
|
continue;
|
|
1037
2332
|
}
|
|
@@ -1060,6 +2355,10 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
|
|
|
1060
2355
|
}
|
|
1061
2356
|
|
|
1062
2357
|
for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
|
|
2358
|
+
const queueSteerSequence = queueSteerSequenceByTurn.get(turnId);
|
|
2359
|
+
if (queueSteerSequence !== undefined) {
|
|
2360
|
+
directChatSequenceByTrigger.set(triggerEventId, queueSteerSequence);
|
|
2361
|
+
}
|
|
1063
2362
|
if (!startSeqByTrigger.has(triggerEventId)) {
|
|
1064
2363
|
const fallbackSeq = fallbackSeqByTurn.get(turnId);
|
|
1065
2364
|
if (fallbackSeq !== undefined) {
|
|
@@ -1071,7 +2370,7 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
|
|
|
1071
2370
|
const cancelledBeforeStartTriggers = new Set<string>();
|
|
1072
2371
|
for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
|
|
1073
2372
|
if (
|
|
1074
|
-
cancelledTurnIds.has(turnId) &&
|
|
2373
|
+
(cancelledTurnIds.has(turnId) || withdrawnTurnIds.has(turnId)) &&
|
|
1075
2374
|
!startSeqByTrigger.has(triggerEventId) &&
|
|
1076
2375
|
!fallbackSeqByTurn.has(turnId)
|
|
1077
2376
|
) {
|
|
@@ -1083,6 +2382,8 @@ function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
|
|
|
1083
2382
|
queuedTurnByTrigger,
|
|
1084
2383
|
startSeqByTrigger,
|
|
1085
2384
|
cancelledBeforeStartTriggers,
|
|
2385
|
+
directChatSequenceByTrigger,
|
|
2386
|
+
explicitQueuedTriggers,
|
|
1086
2387
|
startedTurnIds,
|
|
1087
2388
|
};
|
|
1088
2389
|
}
|
|
@@ -1095,8 +2396,22 @@ function orderTimelineEvents(events: SessionEvent[], prescan: TurnAnchorPrescan)
|
|
|
1095
2396
|
if (event.type !== "user.message") {
|
|
1096
2397
|
continue;
|
|
1097
2398
|
}
|
|
2399
|
+
// Steer is an accepted direction change, not waiting queue copy. Keep it
|
|
2400
|
+
// directly in chat across refresh/reconnect even before its turn starts.
|
|
2401
|
+
const directChatSequence = prescan.directChatSequenceByTrigger.get(event.id);
|
|
2402
|
+
if (directChatSequence !== undefined) {
|
|
2403
|
+
pushInsertion(insertions, directChatSequence, event);
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
1098
2406
|
const queuedTurnId = prescan.queuedTurnByTrigger.get(event.id);
|
|
1099
2407
|
if (!queuedTurnId) {
|
|
2408
|
+
if (prescan.explicitQueuedTriggers.has(event.id)) {
|
|
2409
|
+
const startSeq = prescan.startSeqByTrigger.get(event.id);
|
|
2410
|
+
if (startSeq !== undefined) {
|
|
2411
|
+
pushInsertion(insertions, startSeq, event);
|
|
2412
|
+
}
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
1100
2415
|
pushInsertion(insertions, event.sequence, event);
|
|
1101
2416
|
continue;
|
|
1102
2417
|
}
|
|
@@ -1150,16 +2465,21 @@ function isAgentActivityEvent(type: string): boolean {
|
|
|
1150
2465
|
* steering, or deleting a waiting row must never make its prompt appear in the
|
|
1151
2466
|
* transcript as though inference had begun.
|
|
1152
2467
|
*/
|
|
1153
|
-
function isTurnExecutionEvidence(type: string): boolean {
|
|
2468
|
+
export function isTurnExecutionEvidence(type: string): boolean {
|
|
1154
2469
|
return (
|
|
1155
2470
|
isAgentActivityEvent(type) ||
|
|
1156
2471
|
type === "turn.completed" ||
|
|
1157
2472
|
type === "turn.failed" ||
|
|
2473
|
+
// An admission refusal can settle a queued turn before it ever starts:
|
|
2474
|
+
// its prompt still belongs above the "usage limit reached" row.
|
|
2475
|
+
type === "usage.exhausted" ||
|
|
2476
|
+
type === "turn.superseded" ||
|
|
1158
2477
|
type === "turn.recovery.requested" ||
|
|
1159
2478
|
type === "turn.capacity_waiting" ||
|
|
1160
2479
|
type === "session.requiresAction" ||
|
|
1161
2480
|
type === "tool.auth_needed" ||
|
|
1162
2481
|
type === "credential.auth_needed" ||
|
|
2482
|
+
type.startsWith("turn.startup.phase.") ||
|
|
1163
2483
|
type.startsWith("rig.setup.") ||
|
|
1164
2484
|
type === "codex.capacity.waiting" ||
|
|
1165
2485
|
type === "codex.capacity.resumed" ||
|
|
@@ -1263,9 +2583,39 @@ function foldSettledTurn(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
|
|
|
1263
2583
|
if (collected.length === 0) {
|
|
1264
2584
|
return;
|
|
1265
2585
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
2586
|
+
const collectedLength = collected.length;
|
|
2587
|
+
|
|
2588
|
+
// Resolve only inside this turn's collected suffix. A user/machine-input
|
|
2589
|
+
// boundary or an interleaved foreign turn must never donate its answer.
|
|
2590
|
+
const waitMessages = turnEnd.preserveWaitResponse
|
|
2591
|
+
? collected.filter(
|
|
2592
|
+
(group): group is Extract<TimelineGroup, { kind: "item" }> =>
|
|
2593
|
+
group.kind === "item" &&
|
|
2594
|
+
group.item.kind === "agent-message" &&
|
|
2595
|
+
!group.item.streaming &&
|
|
2596
|
+
group.item.text.trim().length > 0 &&
|
|
2597
|
+
belongsToTurn(group.item, turnEnd.turnId),
|
|
2598
|
+
)
|
|
2599
|
+
: [];
|
|
2600
|
+
// A completed answer outranks later commentary. Otherwise use the latest
|
|
2601
|
+
// visible existing prose, ignoring whitespace and opaque-only stream tails.
|
|
2602
|
+
const waitResponse =
|
|
2603
|
+
waitMessages
|
|
2604
|
+
.slice()
|
|
2605
|
+
.reverse()
|
|
2606
|
+
.find(
|
|
2607
|
+
(group) =>
|
|
2608
|
+
group.item.kind === "agent-message" &&
|
|
2609
|
+
group.item.phase !== "commentary" &&
|
|
2610
|
+
group.item.annotationSource?.eventType === "agent.message.completed",
|
|
2611
|
+
) ?? waitMessages.at(-1);
|
|
2612
|
+
const finalMessage = waitResponse ?? extractFinalAgentMessage(collected, turnEnd);
|
|
2613
|
+
const fallbackMessage =
|
|
2614
|
+
finalMessage || hasOrdinaryFinalAgentMessage(collected, turnEnd)
|
|
2615
|
+
? null
|
|
2616
|
+
: extractLatestCompletedCommentary(collected, turnEnd);
|
|
2617
|
+
const visibleMessage = finalMessage ?? fallbackMessage;
|
|
2618
|
+
const body = visibleMessage ? collected.filter((group) => group !== visibleMessage) : collected;
|
|
1269
2619
|
if (body.length === 0) {
|
|
1270
2620
|
return;
|
|
1271
2621
|
}
|
|
@@ -1293,8 +2643,8 @@ function foldSettledTurn(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
|
|
|
1293
2643
|
|
|
1294
2644
|
groups.splice(
|
|
1295
2645
|
startIndex,
|
|
1296
|
-
|
|
1297
|
-
...(
|
|
2646
|
+
collectedLength,
|
|
2647
|
+
...(visibleMessage ? [turnGroup, visibleMessage] : [turnGroup]),
|
|
1298
2648
|
);
|
|
1299
2649
|
}
|
|
1300
2650
|
|
|
@@ -1303,9 +2653,9 @@ function isTurnBoundary(group: TimelineGroup | undefined): boolean {
|
|
|
1303
2653
|
group?.kind === "turn" ||
|
|
1304
2654
|
(group?.kind === "item" &&
|
|
1305
2655
|
(group.item.kind === "user-message" ||
|
|
2656
|
+
group.item.kind === "human-input" ||
|
|
1306
2657
|
group.item.kind === "context-compaction" ||
|
|
1307
|
-
|
|
1308
|
-
group.item.members.some((member) => member.kind === "media_generation_result")) ||
|
|
2658
|
+
group.item.kind === "machine-input-batch" ||
|
|
1309
2659
|
(group.item.kind === "notice" && group.item.tone === "input")))
|
|
1310
2660
|
);
|
|
1311
2661
|
}
|
|
@@ -1350,6 +2700,43 @@ function extractFinalAgentMessage(
|
|
|
1350
2700
|
return tail;
|
|
1351
2701
|
}
|
|
1352
2702
|
|
|
2703
|
+
function hasOrdinaryFinalAgentMessage(groups: TimelineGroup[], turnEnd: TurnEndItem): boolean {
|
|
2704
|
+
return groups.some((group) => {
|
|
2705
|
+
if (group.kind !== "item" || group.item.kind !== "agent-message") return false;
|
|
2706
|
+
const message = group.item;
|
|
2707
|
+
return (
|
|
2708
|
+
!message.streaming &&
|
|
2709
|
+
message.phase !== "commentary" &&
|
|
2710
|
+
message.text.trim().length > 0 &&
|
|
2711
|
+
belongsToTurn(message, turnEnd.turnId)
|
|
2712
|
+
);
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
function extractLatestCompletedCommentary(
|
|
2717
|
+
groups: TimelineGroup[],
|
|
2718
|
+
turnEnd: TurnEndItem,
|
|
2719
|
+
): Extract<TimelineGroup, { kind: "item" }> | null {
|
|
2720
|
+
for (let index = groups.length - 1; index >= 0; index -= 1) {
|
|
2721
|
+
const group = groups[index];
|
|
2722
|
+
if (group?.kind !== "item" || group.item.kind !== "agent-message") continue;
|
|
2723
|
+
const message = group.item;
|
|
2724
|
+
if (
|
|
2725
|
+
!message.streaming &&
|
|
2726
|
+
message.phase === "commentary" &&
|
|
2727
|
+
message.text.trim().length > 0 &&
|
|
2728
|
+
belongsToTurn(message, turnEnd.turnId)
|
|
2729
|
+
) {
|
|
2730
|
+
return group;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
return null;
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
function belongsToTurn(message: AgentMessageItem, turnId: string | null): boolean {
|
|
2737
|
+
return !message.turnId || !turnId || message.turnId === turnId;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
1353
2740
|
function groupStartedAt(group: TimelineGroup | undefined): string | undefined {
|
|
1354
2741
|
if (!group) {
|
|
1355
2742
|
return undefined;
|
|
@@ -1372,10 +2759,37 @@ function stringValue(value: unknown): string {
|
|
|
1372
2759
|
return typeof value === "string" ? value : "";
|
|
1373
2760
|
}
|
|
1374
2761
|
|
|
2762
|
+
function assistantMessagePhase(value: unknown): AgentMessageItem["phase"] {
|
|
2763
|
+
return value === "commentary" || value === "final_answer" ? value : undefined;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
1375
2766
|
function numberOrNull(value: unknown): number | null {
|
|
1376
2767
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1377
2768
|
}
|
|
1378
2769
|
|
|
2770
|
+
/**
|
|
2771
|
+
* Read the closed provider-rejection record a `summarization_failed` skip
|
|
2772
|
+
* carries. Only bounded identifiers are accepted; a provider message is never
|
|
2773
|
+
* part of the payload and would be ignored here anyway.
|
|
2774
|
+
*/
|
|
2775
|
+
function compactionProviderRejection(
|
|
2776
|
+
payload: Record<string, unknown>,
|
|
2777
|
+
): ContextCompactionItem["providerRejection"] {
|
|
2778
|
+
const record = asRecord(payload.providerRejection);
|
|
2779
|
+
if (typeof record.httpStatus !== "number" || !Number.isFinite(record.httpStatus)) {
|
|
2780
|
+
return null;
|
|
2781
|
+
}
|
|
2782
|
+
const text = (value: unknown): string | null =>
|
|
2783
|
+
typeof value === "string" && value.trim() ? value : null;
|
|
2784
|
+
return {
|
|
2785
|
+
httpStatus: record.httpStatus,
|
|
2786
|
+
type: text(record.type),
|
|
2787
|
+
code: text(record.code),
|
|
2788
|
+
param: text(record.param),
|
|
2789
|
+
requestId: text(record.requestId),
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
|
|
1379
2793
|
function compactionTrigger(payload: Record<string, unknown>): ContextCompactionItem["trigger"] {
|
|
1380
2794
|
const trigger = payload.trigger;
|
|
1381
2795
|
return trigger === "auto" ||
|
|
@@ -1431,10 +2845,17 @@ function machineInputMembers(value: unknown): MachineInputBatchItem["members"] {
|
|
|
1431
2845
|
const kinds = new Set<MachineInputBatchItem["members"][number]["kind"]>([
|
|
1432
2846
|
"scheduled_occurrence",
|
|
1433
2847
|
"goal_continuation",
|
|
2848
|
+
"background_command_result",
|
|
2849
|
+
"session_wait_timeout",
|
|
1434
2850
|
"agent_message",
|
|
1435
2851
|
"agent_steer_instruction",
|
|
1436
2852
|
"child_terminal_result",
|
|
1437
2853
|
"media_generation_result",
|
|
2854
|
+
"child_requires_action",
|
|
2855
|
+
"child_requires_action_resolved",
|
|
2856
|
+
"child_paused",
|
|
2857
|
+
"child_waiting_capacity",
|
|
2858
|
+
"child_progress",
|
|
1438
2859
|
]);
|
|
1439
2860
|
const classifications = new Set<MachineInputBatchItem["members"][number]["classification"]>([
|
|
1440
2861
|
"success",
|
|
@@ -1481,6 +2902,8 @@ const SESSION_STATUSES: readonly SessionStatus[] = [
|
|
|
1481
2902
|
"running",
|
|
1482
2903
|
"idle",
|
|
1483
2904
|
"requires_action",
|
|
2905
|
+
"recovering",
|
|
2906
|
+
"waiting_capacity",
|
|
1484
2907
|
"failed",
|
|
1485
2908
|
"cancelled",
|
|
1486
2909
|
];
|
|
@@ -1624,6 +3047,10 @@ function findOpenCall(
|
|
|
1624
3047
|
if (byId) {
|
|
1625
3048
|
return byId as ToolCallItem | WorkerItem;
|
|
1626
3049
|
}
|
|
3050
|
+
// An explicit provider correlation id is authoritative. Falling back to
|
|
3051
|
+
// an unrelated running call corrupts its status/output when compact pages
|
|
3052
|
+
// or interleaved providers deliver an unmatched historical result.
|
|
3053
|
+
return undefined;
|
|
1627
3054
|
}
|
|
1628
3055
|
return reversed.find(
|
|
1629
3056
|
(item): item is ToolCallItem | WorkerItem => isCall(item) && item.status === "running",
|
|
@@ -1639,16 +3066,58 @@ function findOpenSandbox(items: TimelineItem[], name: string): SandboxItem | und
|
|
|
1639
3066
|
);
|
|
1640
3067
|
}
|
|
1641
3068
|
|
|
1642
|
-
function
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
3069
|
+
function startupPhaseForSandboxOperation(name: string): StartupPhase | null {
|
|
3070
|
+
return SANDBOX_STARTUP_PHASES[name] ?? null;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
function startupPhaseFromPayload(value: unknown): StartupPhase | null {
|
|
3074
|
+
return typeof value === "string" && STARTUP_PHASES.has(value as StartupPhase)
|
|
3075
|
+
? (value as StartupPhase)
|
|
3076
|
+
: null;
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
function startupLifecycleWaitReason(
|
|
3080
|
+
payload: Record<string, unknown>,
|
|
3081
|
+
): StartupPhaseItem["blockedReason"] {
|
|
3082
|
+
if (
|
|
3083
|
+
payload.expectedTransition !== true ||
|
|
3084
|
+
payload.failureStage !== "lifecycle_wait" ||
|
|
3085
|
+
payload.failureCategory !== "drain_capture_wait"
|
|
3086
|
+
) {
|
|
3087
|
+
return undefined;
|
|
1650
3088
|
}
|
|
1651
|
-
|
|
3089
|
+
const code = payload.failureCode;
|
|
3090
|
+
return code === "capture_in_progress" ||
|
|
3091
|
+
code === "rotation_in_progress" ||
|
|
3092
|
+
code === "provider_recovery_in_progress"
|
|
3093
|
+
? code
|
|
3094
|
+
: undefined;
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
const SANDBOX_STARTUP_PHASES: Record<string, StartupPhase> = {
|
|
3098
|
+
"sandbox.provision": "sandbox",
|
|
3099
|
+
"repository-clone": "repository",
|
|
3100
|
+
"file-resource-download": "files",
|
|
3101
|
+
};
|
|
3102
|
+
const STARTUP_PHASES = new Set<StartupPhase>([
|
|
3103
|
+
"queue",
|
|
3104
|
+
"sandbox",
|
|
3105
|
+
"rig",
|
|
3106
|
+
"repository",
|
|
3107
|
+
"files",
|
|
3108
|
+
"tools",
|
|
3109
|
+
"model_preparation",
|
|
3110
|
+
"provider_first_byte",
|
|
3111
|
+
]);
|
|
3112
|
+
|
|
3113
|
+
function elapsedDurationMs(startedAt: string, completedAt: string): number | null {
|
|
3114
|
+
const start = Date.parse(startedAt);
|
|
3115
|
+
const end = Date.parse(completedAt);
|
|
3116
|
+
return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : null;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
function failureMessage(payload: Record<string, unknown>): string | null {
|
|
3120
|
+
return presentFailure(payload).reason;
|
|
1652
3121
|
}
|
|
1653
3122
|
|
|
1654
3123
|
function goalText(payload: Record<string, unknown>): string | null {
|
|
@@ -1665,17 +3134,41 @@ function goalText(payload: Record<string, unknown>): string | null {
|
|
|
1665
3134
|
return null;
|
|
1666
3135
|
}
|
|
1667
3136
|
|
|
3137
|
+
function rememberPendingWaitOutcome(
|
|
3138
|
+
pending: Map<string | null, PendingWaitOutcome>,
|
|
3139
|
+
event: SessionEvent,
|
|
3140
|
+
payload: Record<string, unknown>,
|
|
3141
|
+
eventTurnId: string | null,
|
|
3142
|
+
): void {
|
|
3143
|
+
const reason = stringValue(payload.reason).trim();
|
|
3144
|
+
if (!reason) return;
|
|
3145
|
+
const turnId =
|
|
3146
|
+
eventTurnId || stringValue(payload.waitTurnId) || stringValue(payload.turnId) || null;
|
|
3147
|
+
pending.set(turnId, { id: event.id, reason, occurredAt: event.occurredAt });
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
function waitingOutcomeText(reason: string): string {
|
|
3151
|
+
return /^waiting\b/i.test(reason) ? reason : `Waiting: ${reason}`;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
1668
3154
|
/**
|
|
1669
3155
|
* Agent-owned goal mutations already have an in-cluster tool row. Suppress the
|
|
1670
|
-
* breakaway landmark for those only.
|
|
1671
|
-
* and
|
|
1672
|
-
*
|
|
3156
|
+
* breakaway landmark for those only. Empty wait turns retain the durable hold
|
|
3157
|
+
* reason and surface it after the folded steps. `goal.completed` has no actor
|
|
3158
|
+
* field today and is only emitted by the agent tool, so it is always
|
|
3159
|
+
* suppressed. API / system / create-session / continuation landmarks stay
|
|
3160
|
+
* visible.
|
|
1673
3161
|
*/
|
|
1674
3162
|
function shouldSuppressAgentGoalLandmark(type: string, payload: Record<string, unknown>): boolean {
|
|
1675
3163
|
if (type === "goal.completed") {
|
|
1676
3164
|
return true;
|
|
1677
3165
|
}
|
|
1678
|
-
if (
|
|
3166
|
+
if (
|
|
3167
|
+
type === "goal.set" ||
|
|
3168
|
+
type === "goal.updated" ||
|
|
3169
|
+
type === "goal.paused" ||
|
|
3170
|
+
type === "goal.held"
|
|
3171
|
+
) {
|
|
1679
3172
|
return payload.actor === "agent";
|
|
1680
3173
|
}
|
|
1681
3174
|
return false;
|
|
@@ -1746,7 +3239,7 @@ function capabilityAuthorizationRequest(
|
|
|
1746
3239
|
if (
|
|
1747
3240
|
typeof record.id !== "string" ||
|
|
1748
3241
|
typeof record.name !== "string" ||
|
|
1749
|
-
!["
|
|
3242
|
+
!["mcp", "api", "skill", "plugin"].includes(String(kind)) ||
|
|
1750
3243
|
!["built_in", "library", "configured", "public_registry", "registry", "manual"].includes(
|
|
1751
3244
|
String(source),
|
|
1752
3245
|
) ||
|
|
@@ -1766,6 +3259,29 @@ function capabilityAuthorizationRequest(
|
|
|
1766
3259
|
};
|
|
1767
3260
|
}
|
|
1768
3261
|
|
|
3262
|
+
function customMcpSetupRequest(value: unknown): NonNullable<AuthNeededItem["setupRequest"]> | null {
|
|
3263
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
3264
|
+
const input = value as Record<string, unknown>;
|
|
3265
|
+
if (
|
|
3266
|
+
input.kind !== "mcp" ||
|
|
3267
|
+
typeof input.name !== "string" ||
|
|
3268
|
+
typeof input.rationale !== "string" ||
|
|
3269
|
+
typeof input.endpointUrl !== "string"
|
|
3270
|
+
)
|
|
3271
|
+
return null;
|
|
3272
|
+
try {
|
|
3273
|
+
if (new URL(input.endpointUrl).protocol !== "https:") return null;
|
|
3274
|
+
} catch {
|
|
3275
|
+
return null;
|
|
3276
|
+
}
|
|
3277
|
+
return {
|
|
3278
|
+
kind: "mcp",
|
|
3279
|
+
name: input.name,
|
|
3280
|
+
endpointUrl: input.endpointUrl,
|
|
3281
|
+
rationale: input.rationale,
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
|
|
1769
3285
|
function stringList(value: unknown): string[] {
|
|
1770
3286
|
return Array.isArray(value)
|
|
1771
3287
|
? value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
|