@athenaintel/react 0.48.0 → 0.52.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -10
- package/dist/agent-runtime/src/call-recap.cjs +319 -0
- package/dist/agent-runtime/src/call-recap.cjs.map +1 -0
- package/dist/agent-runtime/src/call-recap.d.ts +142 -0
- package/dist/agent-runtime/src/call-recap.js +319 -0
- package/dist/agent-runtime/src/call-recap.js.map +1 -0
- package/dist/agent-runtime/src/chat-outbox-store.cjs +1 -0
- package/dist/agent-runtime/src/chat-outbox-store.cjs.map +1 -1
- package/dist/agent-runtime/src/chat-outbox-store.d.ts +7 -0
- package/dist/agent-runtime/src/chat-outbox-store.js +1 -0
- package/dist/agent-runtime/src/chat-outbox-store.js.map +1 -1
- package/dist/agent-runtime/src/chat-outbox.cjs +46 -5
- package/dist/agent-runtime/src/chat-outbox.cjs.map +1 -1
- package/dist/agent-runtime/src/chat-outbox.d.ts +29 -0
- package/dist/agent-runtime/src/chat-outbox.js +46 -5
- package/dist/agent-runtime/src/chat-outbox.js.map +1 -1
- package/dist/agent-runtime/src/conversation-index.cjs.map +1 -1
- package/dist/agent-runtime/src/conversation-index.d.ts +7 -0
- package/dist/agent-runtime/src/conversation-index.js.map +1 -1
- package/dist/agent-runtime/src/conversation-list.cjs +2 -1
- package/dist/agent-runtime/src/conversation-list.cjs.map +1 -1
- package/dist/agent-runtime/src/conversation-list.d.ts +19 -3
- package/dist/agent-runtime/src/conversation-list.js +2 -1
- package/dist/agent-runtime/src/conversation-list.js.map +1 -1
- package/dist/agent-runtime/src/desktop-handoff.d.ts +10 -0
- package/dist/agent-runtime/src/edit-command.cjs +21 -0
- package/dist/agent-runtime/src/edit-command.cjs.map +1 -1
- package/dist/agent-runtime/src/edit-command.d.ts +8 -0
- package/dist/agent-runtime/src/edit-command.js +21 -0
- package/dist/agent-runtime/src/edit-command.js.map +1 -1
- package/dist/agent-runtime/src/handoff-arrival.cjs +19 -0
- package/dist/agent-runtime/src/handoff-arrival.cjs.map +1 -1
- package/dist/agent-runtime/src/handoff-arrival.d.ts +18 -3
- package/dist/agent-runtime/src/handoff-arrival.js +20 -1
- package/dist/agent-runtime/src/handoff-arrival.js.map +1 -1
- package/dist/agent-runtime/src/handoff-cursor-id.cjs +6 -0
- package/dist/agent-runtime/src/handoff-cursor-id.cjs.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor-id.d.ts +12 -0
- package/dist/agent-runtime/src/handoff-cursor-id.js +6 -0
- package/dist/agent-runtime/src/handoff-cursor-id.js.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor.cjs +88 -0
- package/dist/agent-runtime/src/handoff-cursor.cjs.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor.d.ts +120 -0
- package/dist/agent-runtime/src/handoff-cursor.js +88 -0
- package/dist/agent-runtime/src/handoff-cursor.js.map +1 -0
- package/dist/agent-runtime/src/index.d.ts +9 -7
- package/dist/agent-runtime/src/interrupts.cjs +28 -1
- package/dist/agent-runtime/src/interrupts.cjs.map +1 -1
- package/dist/agent-runtime/src/interrupts.d.ts +21 -1
- package/dist/agent-runtime/src/interrupts.js +28 -1
- package/dist/agent-runtime/src/interrupts.js.map +1 -1
- package/dist/agent-runtime/src/model-context.cjs +3 -2
- package/dist/agent-runtime/src/model-context.cjs.map +1 -1
- package/dist/agent-runtime/src/model-context.d.ts +6 -1
- package/dist/agent-runtime/src/model-context.js +3 -2
- package/dist/agent-runtime/src/model-context.js.map +1 -1
- package/dist/agent-runtime/src/runtime.d.ts +3 -2
- package/dist/agent-runtime/src/send-recovery.cjs +37 -3
- package/dist/agent-runtime/src/send-recovery.cjs.map +1 -1
- package/dist/agent-runtime/src/send-recovery.d.ts +19 -0
- package/dist/agent-runtime/src/send-recovery.js +37 -3
- package/dist/agent-runtime/src/send-recovery.js.map +1 -1
- package/dist/agent-runtime/src/thread-handoff-flow-machine.d.ts +14 -8
- package/dist/agent-runtime/src/thread-handoff-protocol.d.ts +227 -5
- package/dist/agent-runtime/src/thread-handoff.d.ts +1 -1
- package/dist/agent-runtime/src/use-conversation-list.cjs +3 -2
- package/dist/agent-runtime/src/use-conversation-list.cjs.map +1 -1
- package/dist/agent-runtime/src/use-conversation-list.d.ts +6 -0
- package/dist/agent-runtime/src/use-conversation-list.js +3 -2
- package/dist/agent-runtime/src/use-conversation-list.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-chat-outbox.cjs +21 -1
- package/dist/agent-runtime/src/use-deep-agent-chat-outbox.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-chat-outbox.d.ts +8 -2
- package/dist/agent-runtime/src/use-deep-agent-chat-outbox.js +21 -1
- package/dist/agent-runtime/src/use-deep-agent-chat-outbox.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-runtime.cjs +24 -6
- package/dist/agent-runtime/src/use-deep-agent-runtime.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-runtime.d.ts +17 -0
- package/dist/agent-runtime/src/use-deep-agent-runtime.js +24 -6
- package/dist/agent-runtime/src/use-deep-agent-runtime.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-voice.cjs +44 -9
- package/dist/agent-runtime/src/use-deep-agent-voice.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-voice.d.ts +34 -1
- package/dist/agent-runtime/src/use-deep-agent-voice.js +44 -9
- package/dist/agent-runtime/src/use-deep-agent-voice.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +8 -3
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.d.ts +7 -0
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js +9 -4
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js.map +1 -1
- package/dist/agent-runtime/src/use-thread-handoff-flow.d.ts +13 -2
- package/dist/agent-runtime/src/voice-earcon.cjs +139 -0
- package/dist/agent-runtime/src/voice-earcon.cjs.map +1 -0
- package/dist/agent-runtime/src/voice-earcon.d.ts +38 -0
- package/dist/agent-runtime/src/voice-earcon.js +139 -0
- package/dist/agent-runtime/src/voice-earcon.js.map +1 -0
- package/dist/agent-runtime/src/voice-media.cjs.map +1 -1
- package/dist/agent-runtime/src/voice-media.d.ts +14 -0
- package/dist/agent-runtime/src/voice-media.js.map +1 -1
- package/dist/agent-runtime/src/voice-presentation.cjs +2 -0
- package/dist/agent-runtime/src/voice-presentation.cjs.map +1 -1
- package/dist/agent-runtime/src/voice-presentation.js +2 -0
- package/dist/agent-runtime/src/voice-presentation.js.map +1 -1
- package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
- package/dist/assets/AthenaAssetEmbed.d.ts +11 -5
- package/dist/assets/AthenaAssetEmbed.js.map +1 -1
- package/dist/assets/use-asset-embed.cjs.map +1 -1
- package/dist/assets/use-asset-embed.d.ts +12 -5
- package/dist/assets/use-asset-embed.js.map +1 -1
- package/dist/chat/AthenaChat.cjs +27 -12
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.d.ts +13 -0
- package/dist/chat/AthenaChat.js +27 -12
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/AthenaPlanStrip.cjs +97 -0
- package/dist/chat/AthenaPlanStrip.cjs.map +1 -0
- package/dist/chat/AthenaPlanStrip.d.ts +15 -0
- package/dist/chat/AthenaPlanStrip.js +97 -0
- package/dist/chat/AthenaPlanStrip.js.map +1 -0
- package/dist/chat/StatewireApprovalCard.cjs +41 -6
- package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
- package/dist/chat/StatewireApprovalCard.d.ts +18 -1
- package/dist/chat/StatewireApprovalCard.js +42 -7
- package/dist/chat/StatewireApprovalCard.js.map +1 -1
- package/dist/chat/StatewireQueuedMessages.cjs +56 -6
- package/dist/chat/StatewireQueuedMessages.cjs.map +1 -1
- package/dist/chat/StatewireQueuedMessages.d.ts +14 -0
- package/dist/chat/StatewireQueuedMessages.js +58 -8
- package/dist/chat/StatewireQueuedMessages.js.map +1 -1
- package/dist/chat/WaitingOnYouBanner.cjs +6 -2
- package/dist/chat/WaitingOnYouBanner.cjs.map +1 -1
- package/dist/chat/WaitingOnYouBanner.js +7 -3
- package/dist/chat/WaitingOnYouBanner.js.map +1 -1
- package/dist/chat/chat-voice-controls.cjs +3 -1
- package/dist/chat/chat-voice-controls.cjs.map +1 -1
- package/dist/chat/chat-voice-controls.d.ts +6 -1
- package/dist/chat/chat-voice-controls.js +3 -1
- package/dist/chat/chat-voice-controls.js.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingCard.cjs +4 -2
- package/dist/chat/super-grouping/SuperGroupingCard.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingCard.js +4 -2
- package/dist/chat/super-grouping/SuperGroupingCard.js.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs +4 -2
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.js +4 -2
- package/dist/chat/super-grouping/SuperGroupingTranscript.js.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.cjs +6 -4
- package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.d.ts +9 -1
- package/dist/chat/super-grouping/activity-timeline.js +6 -4
- package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
- package/dist/chat/super-grouping/index.d.ts +1 -1
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.cjs +17 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.cjs.map +1 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.d.ts +8 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.js +17 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.js.map +1 -0
- package/dist/chat-ui/index.cjs +11 -0
- package/dist/chat-ui/index.cjs.map +1 -1
- package/dist/chat-ui/index.d.ts +3 -1
- package/dist/chat-ui/index.js +12 -1
- package/dist/chat-ui/index.js.map +1 -1
- package/dist/chat-ui/messages/from-the-call-card.cjs +124 -0
- package/dist/chat-ui/messages/from-the-call-card.cjs.map +1 -0
- package/dist/chat-ui/messages/from-the-call-card.d.ts +17 -0
- package/dist/chat-ui/messages/from-the-call-card.js +124 -0
- package/dist/chat-ui/messages/from-the-call-card.js.map +1 -0
- package/dist/chat-ui/rich-text/chat-link.cjs +1 -0
- package/dist/chat-ui/rich-text/chat-link.cjs.map +1 -1
- package/dist/chat-ui/rich-text/chat-link.d.ts +4 -0
- package/dist/chat-ui/rich-text/chat-link.js +4 -3
- package/dist/chat-ui/rich-text/chat-link.js.map +1 -1
- package/dist/chat-ui/rich-text/chat-message-body.cjs +1 -0
- package/dist/chat-ui/rich-text/chat-message-body.cjs.map +1 -1
- package/dist/chat-ui/rich-text/chat-message-body.d.ts +12 -0
- package/dist/chat-ui/rich-text/chat-message-body.js +2 -1
- package/dist/chat-ui/rich-text/chat-message-body.js.map +1 -1
- package/dist/chat-ui/thread/chat-thread-shell.cjs +4 -0
- package/dist/chat-ui/thread/chat-thread-shell.cjs.map +1 -1
- package/dist/chat-ui/thread/chat-thread-shell.d.ts +4 -0
- package/dist/chat-ui/thread/chat-thread-shell.js +4 -0
- package/dist/chat-ui/thread/chat-thread-shell.js.map +1 -1
- package/dist/chat-ui/thread/handoff-cursor.cjs +216 -0
- package/dist/chat-ui/thread/handoff-cursor.cjs.map +1 -0
- package/dist/chat-ui/thread/handoff-cursor.d.ts +57 -0
- package/dist/chat-ui/thread/handoff-cursor.js +216 -0
- package/dist/chat-ui/thread/handoff-cursor.js.map +1 -0
- package/dist/chat-ui/thread/statewire-history-edge.cjs +2 -0
- package/dist/chat-ui/thread/statewire-history-edge.cjs.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.d.ts +2 -0
- package/dist/chat-ui/thread/statewire-history-edge.js +3 -1
- package/dist/chat-ui/thread/statewire-history-edge.js.map +1 -1
- package/dist/collab/client.cjs +15 -3
- package/dist/collab/client.cjs.map +1 -1
- package/dist/collab/client.d.ts +4 -2
- package/dist/collab/client.js +16 -4
- package/dist/collab/client.js.map +1 -1
- package/dist/composer/index.d.ts +1 -1
- package/dist/composer/useReadComposerDraft.cjs +26 -7
- package/dist/composer/useReadComposerDraft.cjs.map +1 -1
- package/dist/composer/useReadComposerDraft.d.ts +23 -1
- package/dist/composer/useReadComposerDraft.js +27 -8
- package/dist/composer/useReadComposerDraft.js.map +1 -1
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs +207 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs.map +1 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.d.ts +141 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.js +207 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.js.map +1 -0
- package/dist/keryx-http/src/update-attributions.cjs +34 -4
- package/dist/keryx-http/src/update-attributions.cjs.map +1 -1
- package/dist/keryx-http/src/update-attributions.d.ts +36 -4
- package/dist/keryx-http/src/update-attributions.js +35 -5
- package/dist/keryx-http/src/update-attributions.js.map +1 -1
- package/dist/provider/AthenaProvider.cjs +4 -0
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.d.ts +11 -1
- package/dist/provider/AthenaProvider.js +4 -0
- package/dist/provider/AthenaProvider.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/threads/api.cjs.map +1 -1
- package/dist/threads/api.d.ts +15 -0
- package/dist/threads/api.js.map +1 -1
- package/dist/threads/conversation-list-fetcher.cjs +13 -5
- package/dist/threads/conversation-list-fetcher.cjs.map +1 -1
- package/dist/threads/conversation-list-fetcher.d.ts +4 -1
- package/dist/threads/conversation-list-fetcher.js +13 -5
- package/dist/threads/conversation-list-fetcher.js.map +1 -1
- package/dist/threads/statewire-thread-list.cjs +28 -6
- package/dist/threads/statewire-thread-list.cjs.map +1 -1
- package/dist/threads/statewire-thread-list.d.ts +20 -8
- package/dist/threads/statewire-thread-list.js +28 -6
- package/dist/threads/statewire-thread-list.js.map +1 -1
- package/dist/threads/use-athena-sessions.cjs +27 -5
- package/dist/threads/use-athena-sessions.cjs.map +1 -1
- package/dist/threads/use-athena-sessions.d.ts +10 -5
- package/dist/threads/use-athena-sessions.js +28 -6
- package/dist/threads/use-athena-sessions.js.map +1 -1
- package/dist/threads/use-rest-conversation-history.d.ts +1 -0
- package/dist/tools/write-todos-tool-ui.cjs +8 -0
- package/dist/tools/write-todos-tool-ui.cjs.map +1 -1
- package/dist/tools/write-todos-tool-ui.d.ts +2 -0
- package/dist/tools/write-todos-tool-ui.js +8 -0
- package/dist/tools/write-todos-tool-ui.js.map +1 -1
- package/dist/workbench/session-workbench-panes.cjs +1 -8
- package/dist/workbench/session-workbench-panes.cjs.map +1 -1
- package/dist/workbench/session-workbench-panes.js +2 -9
- package/dist/workbench/session-workbench-panes.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation-list.cjs","sources":["../../../../agent-runtime/src/conversation-list.ts"],"sourcesContent":["import type { InfiniteData, QueryClient } from '@tanstack/react-query';\nimport type { ConversationIndexEntry } from './conversation-index';\n\n/**\n * What a session asks of the viewer, derived server-side from the run, the\n * parked interrupt, the pull requests it opened, its termination and its last\n * wake (the Devin-style session list). `needs_you` is the only state that\n * demands the viewer act; `working` is live; `ready` finished with something\n * to look at; `watching` waits on an external party (a review, a check);\n * `done` and `error` are terminal. A backend newer than this client may report\n * a value outside the union — render it as \"no attention\" rather than failing.\n */\nexport type ConversationAttention =\n | 'needs_you'\n | 'working'\n | 'ready'\n | 'watching'\n | 'done'\n | 'error';\n\nexport const CONVERSATION_ATTENTIONS: readonly ConversationAttention[] = [\n 'needs_you',\n 'working',\n 'ready',\n 'watching',\n 'done',\n 'error',\n];\n\n/** Which signal produced the attention state (and the `nextAction` copy). */\nexport type ConversationAttentionSource =\n | 'interrupt'\n | 'run'\n | 'pull_request'\n | 'termination'\n | 'wake'\n | 'handoff'\n | 'follow_up';\n\n/** The last event that woke a resting session (a GitHub webhook, a schedule, a handoff). */\nexport interface ConversationWake {\n source: string | null;\n reason: string | null;\n /** ISO 8601 timestamp of the wake. */\n at: string | null;\n}\n\n/**\n * Live GitHub state of a pull request the session opened, read by the server\n * when the list renders. Never pushed by the conversation index — it comes\n * from the list fetch only, so a fold keeps the fetched value.\n */\nexport interface ConversationPullRequest {\n /** `owner/repo` the pull request lives in. */\n repoFullName: string;\n number: number;\n url: string;\n title: string;\n state: 'OPEN' | 'CLOSED' | 'MERGED';\n isDraft: boolean;\n /** GitHub's review decision (APPROVED, CHANGES_REQUESTED, REVIEW_REQUIRED) or null. */\n reviewDecision: string | null;\n /** Combined state of the checks on the head commit. */\n checksState: 'SUCCESS' | 'FAILURE' | 'PENDING' | 'NONE';\n /** ISO 8601 timestamp of the pull request's last update on GitHub. */\n updatedAt: string;\n}\n\n/**\n * Shared conversation/thread LIST contract (chat-stack unification —\n * thread-list-contract-convergence).\n *\n * Every chat surface lists \"my conversations\" against the same underlying\n * session assets, but historically each declared its own row shape, paging\n * model, and filter set (Olympus GraphQL `listConversations`, the SDK/Chrome\n * REST `/api/conversations/threads/list`, mobile's `/api/v0/sessions`). This\n * module is the ONE contract those surfaces converge on:\n *\n * - **Row shape**: {@link ConversationSummary} — the canonical camelCase\n * superset. Fields a transport cannot supply are `null`, never invented.\n * - **Ordering**: newest first by `updatedAt`, always. Ordering is part of the\n * contract, not a parameter — no surface sorts any other way.\n * - **Paging**: offset/limit with a LIVE newest page and explicitly-fetched\n * frozen history pages (see `useConversationList`). `hasMore` continues the\n * scan; `totalCount` is advisory and may be `null` for transports that do\n * not count.\n * - **Filtering**: {@link ConversationListFilter}. `appId` follows\n * `docs/SESSION_APP_ID_GUIDE.md`: exact match when set (legacy `null`\n * sessions excluded), no filter when unset. A fetcher that cannot express a\n * requested filter field MUST throw rather than silently ignore it — a\n * dropped filter returns wrong rows under a cache key that claims otherwise.\n * - **Freshness**: the per-user conversation-index statewire\n * (`conversation-index.ts`) is the push signal; these queries are the data\n * source. Push handlers call {@link invalidateConversationListCaches}.\n */\nexport interface ConversationSummary {\n /** LangGraph thread id — the cross-surface conversation identity. */\n threadId: string;\n title: string;\n /** ISO 8601 timestamp of last update (the contract's sort key). */\n updatedAt: string;\n /** ISO 8601 creation timestamp, when the transport reports it. */\n createdAt: string | null;\n /** Session lifecycle state from athena_metadata (e.g. running, completed). */\n state: string | null;\n /** Agent runtime marker (e.g. 'athena_deep_agent'), when reported. */\n runtime: string | null;\n /** Channel the session was started from (e.g. agent_email, web). */\n sourceChannel: string | null;\n /** Asset type (session, video_session, desktop_session, mobile_session). */\n assetType: string | null;\n /** Session asset id (`asset_<uuid>`), when the transport reports it. */\n assetId: string | null;\n /** Client app that created the session (SESSION_APP_ID_GUIDE). */\n appId: string | null;\n /** Computer asset whose sandbox started the session (`sourceChannel` 'computer'); omitted by surfaces that don't carry it. */\n computerAssetId?: string | null;\n /**\n * Collab agent (`asset_<uuid>`) and channel the session started on; `null`\n * channel is the agent's default channel. The server refuses a run on another\n * of the agent's channels, so a client shows the session's mode from these\n * instead of remembering it. Omitted by surfaces that don't carry them.\n */\n collabAgentId?: string | null;\n collabChannelId?: string | null;\n /** Session asset id this row is a sub-session of; omitted by surfaces that don't carry it. */\n parentSessionId?: string | null;\n /** Sub-sessions of this row the viewer can see; omitted by surfaces that don't carry it. */\n subSessionCount?: number;\n /** Trigger marker (`manual` remains user-initiated; other values are automated). */\n triggerType: string | null;\n /** Last-message preview used when a legacy session has no useful title. */\n preview: string | null;\n messageCount: number | null;\n /** Per-user unread flag derived server-side from the read receipt. */\n isUnread: boolean;\n /**\n * Canonical session status (status_v2: idle | active | needs_input | error),\n * or null when the transport does not report it.\n */\n sessionStatus: string | null;\n /**\n * Canonical status of the session's current run (status_v2: scheduled |\n * queued | running | needs_input | completed | failed | canceled). Null when\n * the transport does not report it — {@link isConversationRunning} then\n * falls back to the legacy `state`. Running badges on every surface derive\n * from this one helper so they cannot disagree.\n */\n runStatus: string | null;\n /**\n * Server-derived attention state. `undefined` when the transport (or an\n * older server) does not report it — {@link deriveConversationAttention}\n * then falls back to the status planes; `null` when the server reports none.\n */\n attention?: ConversationAttention | null;\n /** One line (≤ 80 chars) of what the viewer should do next, when the server derives one. */\n nextAction?: string | null;\n /** Which signal produced `attention` / `nextAction`. */\n attentionSource?: ConversationAttentionSource | null;\n /** Whether the viewer pinned (favorited) the session; `undefined` when not reported. */\n isFavorited?: boolean;\n /**\n * Whether the viewer started the session (`false`: it is shared with them);\n * `undefined` when not reported. Never pushed by the conversation index, so\n * a fold keeps the fetched value.\n */\n createdByMe?: boolean;\n /** The last wake of a resting session; `undefined` when not reported. */\n lastWake?: ConversationWake | null;\n /**\n * Pull requests the session opened with their live GitHub state. `[]` when\n * it opened none, `null` when the state could not be read right now,\n * `undefined` when the transport did not ask for (or report) it.\n */\n pullRequests?: ConversationPullRequest[] | null;\n}\n\n/**\n * The canonical filter set. Semantics are defined here once; each fetcher\n * translates the fields its transport supports and throws on the rest.\n */\nexport interface ConversationListFilter {\n /** Exact-match client app filter; unset = all apps (SESSION_APP_ID_GUIDE). */\n appId?: string;\n /**\n * Sessions of any of these client apps too, a union with `appId` (REST thread\n * list only today). A host lists an earlier app's sessions beside its own.\n */\n appIds?: readonly string[];\n /** Asset types to include (e.g. ['session']); unset = all session types. */\n assetTypes?: readonly string[];\n /** Case-insensitive title search. */\n search?: string;\n /** Hide triggered/background sessions (REST thread list only today). */\n excludeTriggered?: boolean;\n /** Start-channel filter (v0 sessions / REST thread list only today). */\n sourceChannels?: readonly string[];\n /** Start channels to hide (e.g. ['api']); GraphQL `listConversations` only today. */\n excludeSourceChannels?: readonly string[];\n /** Exact computer asset whose sandbox started the session; GraphQL `listConversations` only today. */\n computerAssetId?: string;\n /** Only the sub-sessions of this session asset id; GraphQL `listConversations` only today. */\n parentSessionId?: string;\n /**\n * Lifecycle-state filter (e.g. ['running'], ['completed']). Transports match\n * it with the shared status_v2 rule (running carries a 12-hour recency cutoff).\n */\n states?: readonly string[];\n /** Restrict to sessions run by any of these agents (athena_metadata.agent). */\n agentIds?: readonly string[];\n /**\n * Include branched sub-sessions and orchestration children, which every list\n * hides by default because they belong under their parent thread.\n */\n includeSubSessions?: boolean;\n}\n\nexport interface ConversationListPage {\n /** Rows in contract order (updatedAt desc). */\n items: ConversationSummary[];\n /** Whether another `offset + limit` scan would return more rows. */\n hasMore: boolean;\n /** Total matching rows, or null when the transport does not count. */\n totalCount: number | null;\n}\n\nexport interface ConversationListFetchParams {\n limit: number;\n offset: number;\n filter: ConversationListFilter;\n signal?: AbortSignal;\n}\n\n/**\n * One page fetch against some transport (GraphQL `listConversations`, the\n * REST thread list, …). Must reject on filter fields it cannot express.\n */\nexport type ConversationListFetcher = (\n params: ConversationListFetchParams\n) => Promise<ConversationListPage>;\n\n/** Root of every conversation-list query key (all surfaces, all filters). */\nexport const CONVERSATION_LIST_QUERY_KEY_ROOT = 'athena-conversation-list';\n\n/**\n * Scope used by `useConversationList` when the caller passes no `scopeKey`.\n * Exported so cache seeders (e.g. Olympus's spacesBootstrap adoption) can\n * address the exact keys default-scoped hooks read from.\n */\nexport const CONVERSATION_LIST_DEFAULT_SCOPE_KEY = 'default';\n\n/**\n * Normalize a filter to its semantic content: unset/empty fields are dropped\n * (so `{}` and `{ appId: undefined }` are the same filter) and array order is\n * canonicalized. Fetchers receive the normalized form, so what a cache entry\n * claims to hold is exactly what was fetched.\n */\nexport function normalizeConversationListFilter(\n filter: ConversationListFilter\n): ConversationListFilter {\n const normalized: ConversationListFilter = {};\n if (filter.appId) normalized.appId = filter.appId;\n if (filter.appIds && filter.appIds.length > 0) {\n normalized.appIds = [...new Set(filter.appIds)].sort();\n }\n if (filter.assetTypes && filter.assetTypes.length > 0) {\n normalized.assetTypes = [...filter.assetTypes].sort();\n }\n if (filter.search) normalized.search = filter.search;\n if (filter.excludeTriggered !== undefined) normalized.excludeTriggered = filter.excludeTriggered;\n if (filter.sourceChannels && filter.sourceChannels.length > 0) {\n normalized.sourceChannels = [...filter.sourceChannels].sort();\n }\n if (filter.excludeSourceChannels && filter.excludeSourceChannels.length > 0) {\n normalized.excludeSourceChannels = [...filter.excludeSourceChannels].sort();\n }\n if (filter.computerAssetId) normalized.computerAssetId = filter.computerAssetId;\n if (filter.parentSessionId) normalized.parentSessionId = filter.parentSessionId;\n if (filter.states && filter.states.length > 0) {\n normalized.states = [...filter.states].sort();\n }\n if (filter.agentIds && filter.agentIds.length > 0) {\n normalized.agentIds = [...filter.agentIds].sort();\n }\n if (filter.includeSubSessions) normalized.includeSubSessions = true;\n return normalized;\n}\n\n/** status_v2 run statuses during which a session is actively working. */\nconst RUNNING_RUN_STATUSES: ReadonlySet<string> = new Set(['queued', 'running']);\n\n/**\n * THE running rule for every conversation-list surface (sidebar pane, recent\n * dropdown, workspace section, pushed index rows): the canonical status_v2 run\n * status when the transport reports one, else the legacy `state`. A session\n * paused on a human gate (`needs_input`) is not running.\n */\nexport function isConversationRunning(summary: {\n runStatus?: string | null;\n state?: string | null;\n}): boolean {\n if (summary.runStatus !== null && summary.runStatus !== undefined) {\n return RUNNING_RUN_STATUSES.has(summary.runStatus);\n }\n return summary.state === 'running';\n}\n\nconst KNOWN_ATTENTIONS: ReadonlySet<string> = new Set(CONVERSATION_ATTENTIONS);\n\n/**\n * THE attention rule for every session-list surface: the server-derived\n * `attention` when the transport reports one it knows, else a client fallback\n * from the status planes so an older server still yields the two states a\n * list cannot do without — a human gate reads as `needs_you`, a live run as\n * `working`, a failed run as `error`. `null` when nothing applies.\n */\nexport function deriveConversationAttention(\n summary: Pick<ConversationSummary, 'attention' | 'runStatus' | 'sessionStatus' | 'state'>\n): ConversationAttention | null {\n if (typeof summary.attention === 'string' && KNOWN_ATTENTIONS.has(summary.attention)) {\n return summary.attention;\n }\n if (summary.runStatus === 'needs_input' || summary.sessionStatus === 'needs_input') {\n return 'needs_you';\n }\n if (isConversationRunning(summary)) return 'working';\n if (summary.runStatus === 'failed' || summary.sessionStatus === 'error') return 'error';\n return null;\n}\n\n/**\n * Stable cache identity for a filter: the normalized fields serialized in\n * sorted key order, so semantically equal filters share one cache entry.\n */\nexport function conversationListFilterKey(filter: ConversationListFilter): string {\n const normalized = normalizeConversationListFilter(filter) as Record<string, unknown>;\n return JSON.stringify(\n Object.keys(normalized)\n .sort()\n .map((key) => [key, normalized[key]])\n );\n}\n\n/**\n * Query keys for the two-tier pager. `scopeKey` isolates caches beyond the\n * filter — surfaces MUST include everything that changes whose list this is\n * (principal/user id, backend base URL) so a credential swap can never read\n * another identity's rows out of the cache.\n *\n * `pageSize` is part of the key because it is baked into every cached page's\n * offset boundaries: two hooks sharing a scope and filter but paging by\n * different sizes would otherwise reuse each other's incompatible boundaries\n * and skip or duplicate rows.\n */\nexport const conversationListQueryKeys = {\n root: [CONVERSATION_LIST_QUERY_KEY_ROOT] as const,\n scope: (scopeKey: string) => [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey] as const,\n firstPage: (scopeKey: string, filterKey: string, pageSize: number) =>\n [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey, filterKey, pageSize, 'first-page'] as const,\n history: (scopeKey: string, filterKey: string, pageSize: number) =>\n [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey, filterKey, pageSize, 'history'] as const,\n};\n\n/**\n * Flatten pages into one newest-first list, deduplicating by `threadId`.\n * The live first page wins over retained history copies of the same thread.\n */\nexport function mergeConversationPages(\n pages: readonly ConversationListPage[]\n): ConversationSummary[] {\n const seen = new Set<string>();\n const items: ConversationSummary[] = [];\n for (const page of pages) {\n for (const item of page.items) {\n if (seen.has(item.threadId)) continue;\n seen.add(item.threadId);\n items.push(item);\n }\n }\n return items;\n}\n\n/**\n * Prepend a conversation to a page unless its thread is already present\n * (optimistic \"new thread appears at the top\" write). Identity-preserving\n * no-op when present, so equal writes never notify subscribers.\n */\nexport function prependConversationToPage(\n page: ConversationListPage,\n item: ConversationSummary\n): ConversationListPage {\n if (page.items.some((existing) => existing.threadId === item.threadId)) return page;\n return {\n ...page,\n items: [item, ...page.items],\n totalCount: page.totalCount === null ? null : page.totalCount + 1,\n };\n}\n\n/** Remove a thread's row from a page; identity-preserving when absent. */\nexport function removeConversationFromPage(\n page: ConversationListPage,\n threadId: string\n): ConversationListPage {\n if (!page.items.some((item) => item.threadId === threadId)) return page;\n return { ...page, items: page.items.filter((item) => item.threadId !== threadId) };\n}\n\n/**\n * Patch every row matching `match`; identity-preserving when nothing matches\n * so cache writes cannot cause spurious re-renders.\n */\nexport function patchConversationsInPage(\n page: ConversationListPage,\n match: (item: ConversationSummary) => boolean,\n patch: Partial<ConversationSummary>\n): ConversationListPage {\n let changed = false;\n const items = page.items.map((item) => {\n if (!match(item)) return item;\n changed = true;\n return { ...item, ...patch };\n });\n return changed ? { ...page, items } : page;\n}\n\ntype CachedConversationList =\n | ConversationListPage\n | InfiniteData<ConversationListPage, number>\n | undefined;\n\nfunction isInfinitePages(\n data: ConversationListPage | InfiniteData<ConversationListPage, number>\n): data is InfiniteData<ConversationListPage, number> {\n return 'pages' in data;\n}\n\nfunction mapCachedPages(\n data: CachedConversationList,\n transform: (page: ConversationListPage) => ConversationListPage\n): CachedConversationList {\n if (!data) return data;\n if (isInfinitePages(data)) {\n let changed = false;\n const pages = data.pages.map((page) => {\n const next = transform(page);\n if (next !== page) changed = true;\n return next;\n });\n return changed ? { ...data, pages } : data;\n }\n return transform(data);\n}\n\ninterface ConversationListCacheScope {\n /** Restrict to one scope; omitted = every conversation-list cache. */\n scopeKey?: string;\n}\n\nfunction scopedKey(options?: ConversationListCacheScope): readonly string[] {\n return options?.scopeKey !== undefined\n ? conversationListQueryKeys.scope(options.scopeKey)\n : conversationListQueryKeys.root;\n}\n\n/**\n * Invalidate conversation-list caches (all filters, first page + history).\n * This is the hook for push signals — the conversation-index statewire, a\n * finished run, an archive/restore — anything that changed the server list.\n */\nexport function invalidateConversationListCaches(\n queryClient: QueryClient,\n options?: ConversationListCacheScope\n): Promise<void> {\n return queryClient.invalidateQueries({ queryKey: scopedKey(options) });\n}\n\n/** Cancel in-flight list fetches so an optimistic write cannot be overwritten. */\nexport function cancelConversationListCaches(\n queryClient: QueryClient,\n options?: ConversationListCacheScope\n): Promise<void> {\n return queryClient.cancelQueries({ queryKey: scopedKey(options) });\n}\n\n/**\n * Patch matching rows in every cached conversation list (first pages, history\n * pages, searches) so per-row state (read receipts, titles) flips in the same\n * frame as the interaction instead of after the next refetch.\n */\nexport function patchConversationListCaches(\n queryClient: QueryClient,\n options: ConversationListCacheScope & {\n match: (item: ConversationSummary) => boolean;\n patch: Partial<ConversationSummary>;\n }\n): void {\n queryClient.setQueriesData<CachedConversationList>({ queryKey: scopedKey(options) }, (data) =>\n mapCachedPages(data, (page) => patchConversationsInPage(page, options.match, options.patch))\n );\n}\n\n/** Remove a thread's row from every cached conversation list in scope. */\nexport function removeConversationFromListCaches(\n queryClient: QueryClient,\n options: ConversationListCacheScope & { threadId: string }\n): void {\n queryClient.setQueriesData<CachedConversationList>({ queryKey: scopedKey(options) }, (data) =>\n mapCachedPages(data, (page) => removeConversationFromPage(page, options.threadId))\n );\n}\n\nfunction timestampMs(value: string): number {\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? 0 : parsed;\n}\n\nfunction sameSummary(a: ConversationSummary, b: ConversationSummary): boolean {\n return (\n a.threadId === b.threadId &&\n a.title === b.title &&\n a.updatedAt === b.updatedAt &&\n a.createdAt === b.createdAt &&\n a.state === b.state &&\n a.runtime === b.runtime &&\n a.sourceChannel === b.sourceChannel &&\n a.assetType === b.assetType &&\n a.assetId === b.assetId &&\n a.appId === b.appId &&\n a.triggerType === b.triggerType &&\n a.preview === b.preview &&\n a.messageCount === b.messageCount &&\n a.isUnread === b.isUnread &&\n a.sessionStatus === b.sessionStatus &&\n a.runStatus === b.runStatus &&\n a.attention === b.attention &&\n a.nextAction === b.nextAction &&\n a.attentionSource === b.attentionSource &&\n a.isFavorited === b.isFavorited &&\n // Never pushed by the index, so a fold carries the previous references\n // through unchanged; identity is the equality that matters here.\n a.lastWake === b.lastWake &&\n a.pullRequests === b.pullRequests\n );\n}\n\n/**\n * Project one replicated conversation-index row onto the shared summary shape.\n * Fields the index never carries (`createdAt`) and viewer-specific fields a\n * lifecycle publish cannot know (`is_unread: null`) keep the row's previous\n * value. Canonical status is taken from the entry when the publishing pod\n * reports it; a snapshot from a pod that predates the fields falls back to the\n * legacy `state` (null status) rather than keeping a stale run status.\n *\n * Attention fields (`attention`, `next_action`, `attention_source`,\n * `is_favorited`) are taken from the entry when the publishing pod reports\n * them, so a live push flips a row to \"needs you\" the way it renames it; a\n * pod that predates them leaves the row's previous values. Pull-request state,\n * the last wake and `createdByMe` are never pushed and always keep the fetched\n * value.\n */\nexport function summaryFromConversationIndexEntry(\n entry: ConversationIndexEntry,\n previous?: ConversationSummary\n): ConversationSummary {\n const carriesStatus = entry.run_status !== undefined || entry.session_status !== undefined;\n return {\n threadId: entry.thread_id,\n title: entry.title,\n updatedAt: entry.updated_at,\n createdAt: previous?.createdAt ?? null,\n state: entry.state,\n runtime: entry.runtime,\n sourceChannel: entry.start_channel,\n assetType: entry.asset_type,\n assetId: entry.asset_id ?? previous?.assetId ?? null,\n computerAssetId: previous?.computerAssetId ?? null,\n collabAgentId: previous?.collabAgentId,\n collabChannelId: previous?.collabChannelId,\n parentSessionId: previous?.parentSessionId,\n subSessionCount: previous?.subSessionCount,\n appId: entry.app_id,\n triggerType: entry.trigger_type,\n preview: entry.last_message_preview,\n messageCount: entry.message_count,\n isUnread: entry.is_unread ?? previous?.isUnread ?? false,\n sessionStatus: carriesStatus ? (entry.session_status ?? null) : null,\n runStatus: carriesStatus ? (entry.run_status ?? null) : null,\n attention: entry.attention !== undefined ? entry.attention : previous?.attention,\n nextAction: entry.next_action !== undefined ? entry.next_action : previous?.nextAction,\n attentionSource:\n entry.attention_source !== undefined ? entry.attention_source : previous?.attentionSource,\n isFavorited: entry.is_favorited ?? previous?.isFavorited,\n createdByMe: previous?.createdByMe,\n lastWake: previous?.lastWake,\n pullRequests: previous?.pullRequests,\n };\n}\n\n/**\n * Fold the user's replicated conversation index (the newest-page window the\n * per-user statewire pushes) into a cached live first page, so a thread\n * created, renamed, finished, or read anywhere updates every list surface in\n * the same frame — without refetching the page.\n *\n * - Rows in the window are upserted (new threads appear, existing rows take\n * the pushed fields, see {@link summaryFromConversationIndexEntry}).\n * - Rows NOT in the window but at least as new as the window's oldest row\n * should have been in it, so they left the list (archived, hidden, shared\n * away) and are dropped. Older rows are beyond the window and are kept.\n * - When the index reports no further rows (`indexHasMore === false`) the\n * window IS the whole list, so every row outside it is dropped.\n *\n * Newest-first ordering is restored and the page identity is preserved when\n * nothing changed, so an equal push never notifies subscribers.\n */\nexport function mergeConversationIndexIntoPage(\n page: ConversationListPage,\n entries: readonly ConversationIndexEntry[],\n options: { indexHasMore: boolean }\n): ConversationListPage {\n const previousByThread = new Map(page.items.map((item) => [item.threadId, item]));\n const inWindow = new Set(entries.map((entry) => entry.thread_id));\n const windowTailMs =\n entries.length > 0 ? Math.min(...entries.map((entry) => timestampMs(entry.updated_at))) : null;\n\n const merged: ConversationSummary[] = entries.map((entry) => {\n const previous = previousByThread.get(entry.thread_id);\n const next = summaryFromConversationIndexEntry(entry, previous);\n return previous && sameSummary(previous, next) ? previous : next;\n });\n\n for (const item of page.items) {\n if (inWindow.has(item.threadId)) continue;\n if (!options.indexHasMore) continue;\n if (windowTailMs !== null && timestampMs(item.updatedAt) >= windowTailMs) continue;\n merged.push(item);\n }\n\n merged.sort((a, b) => timestampMs(b.updatedAt) - timestampMs(a.updatedAt));\n\n const unchanged =\n merged.length === page.items.length &&\n merged.every((item, index) => item === page.items[index]);\n if (unchanged) return page;\n\n return { ...page, items: merged };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"conversation-list.cjs","sources":["../../../../agent-runtime/src/conversation-list.ts"],"sourcesContent":["import type { InfiniteData, QueryClient } from '@tanstack/react-query';\nimport type { ConversationIndexEntry } from './conversation-index';\n\n/**\n * What a session asks of the viewer, derived server-side from the run, the\n * parked interrupt, the pull requests it opened, its termination and its last\n * wake (the Devin-style session list). `needs_you` is the only state that\n * demands the viewer act; `working` is live; `ready` finished with something\n * to look at; `watching` waits on an external party (a review, a check);\n * `done` and `error` are terminal. A backend newer than this client may report\n * a value outside the union — render it as \"no attention\" rather than failing.\n */\nexport type ConversationAttention =\n | 'needs_you'\n | 'working'\n | 'ready'\n | 'watching'\n | 'done'\n | 'error';\n\nexport const CONVERSATION_ATTENTIONS: readonly ConversationAttention[] = [\n 'needs_you',\n 'working',\n 'ready',\n 'watching',\n 'done',\n 'error',\n];\n\n/** Which signal produced the attention state (and the `nextAction` copy). */\nexport type ConversationAttentionSource =\n | 'interrupt'\n | 'run'\n | 'pull_request'\n | 'termination'\n | 'wake'\n | 'handoff'\n | 'follow_up';\n\n/** The last event that woke a resting session (a GitHub webhook, a schedule, a handoff). */\nexport interface ConversationWake {\n source: string | null;\n reason: string | null;\n /** ISO 8601 timestamp of the wake. */\n at: string | null;\n}\n\n/**\n * Live GitHub state of a pull request the session opened, read by the server\n * when the list renders. Never pushed by the conversation index — it comes\n * from the list fetch only, so a fold keeps the fetched value.\n */\nexport interface ConversationPullRequest {\n /** `owner/repo` the pull request lives in. */\n repoFullName: string;\n number: number;\n url: string;\n title: string;\n state: 'OPEN' | 'CLOSED' | 'MERGED';\n isDraft: boolean;\n /** GitHub's review decision (APPROVED, CHANGES_REQUESTED, REVIEW_REQUIRED) or null. */\n reviewDecision: string | null;\n /** Combined state of the checks on the head commit. */\n checksState: 'SUCCESS' | 'FAILURE' | 'PENDING' | 'NONE';\n /** ISO 8601 timestamp of the pull request's last update on GitHub. */\n updatedAt: string;\n}\n\n/**\n * Shared conversation/thread LIST contract (chat-stack unification —\n * thread-list-contract-convergence).\n *\n * Every chat surface lists \"my conversations\" against the same underlying\n * session assets, but historically each declared its own row shape, paging\n * model, and filter set (Olympus GraphQL `listConversations`, the SDK/Chrome\n * REST `/api/conversations/threads/list`, mobile's `/api/v0/sessions`). This\n * module is the ONE contract those surfaces converge on:\n *\n * - **Row shape**: {@link ConversationSummary} — the canonical camelCase\n * superset. Fields a transport cannot supply are `null`, never invented.\n * - **Ordering**: newest first by `updatedAt`, always. Ordering is part of the\n * contract, not a parameter — no surface sorts any other way.\n * - **Paging**: offset/limit with a LIVE newest page and explicitly-fetched\n * frozen history pages (see `useConversationList`). `hasMore` continues the\n * scan; `totalCount` is advisory and may be `null` for transports that do\n * not count.\n * - **Filtering**: {@link ConversationListFilter}. `appId` follows\n * `docs/SESSION_APP_ID_GUIDE.md`: exact match when set (legacy `null`\n * sessions excluded), no filter when unset. A fetcher that cannot express a\n * requested filter field MUST throw rather than silently ignore it — a\n * dropped filter returns wrong rows under a cache key that claims otherwise.\n * - **Freshness**: the per-user conversation-index statewire\n * (`conversation-index.ts`) is the push signal; these queries are the data\n * source. Push handlers call {@link invalidateConversationListCaches}.\n */\nexport interface ConversationSummary {\n /** LangGraph thread id — the cross-surface conversation identity. */\n threadId: string;\n title: string;\n /** ISO 8601 timestamp of last update (the contract's sort key). */\n updatedAt: string;\n /** ISO 8601 creation timestamp, when the transport reports it. */\n createdAt: string | null;\n /** Session lifecycle state from athena_metadata (e.g. running, completed). */\n state: string | null;\n /** Agent runtime marker (e.g. 'athena_deep_agent'), when reported. */\n runtime: string | null;\n /** Channel the session was started from (e.g. agent_email, web). */\n sourceChannel: string | null;\n /** Asset type (session, video_session, desktop_session, mobile_session). */\n assetType: string | null;\n /** Session asset id (`asset_<uuid>`), when the transport reports it. */\n assetId: string | null;\n /** Client app that created the session (SESSION_APP_ID_GUIDE). */\n appId: string | null;\n /** Computer asset whose sandbox started the session (`sourceChannel` 'computer'); omitted by surfaces that don't carry it. */\n computerAssetId?: string | null;\n /**\n * Collab agent (`asset_<uuid>`) and channel the session started on; `null`\n * channel is the agent's default channel. The server refuses a run on another\n * of the agent's channels, so a client shows the session's mode from these\n * instead of remembering it. Omitted by surfaces that don't carry them.\n */\n collabAgentId?: string | null;\n collabChannelId?: string | null;\n /** Session asset id this row is a sub-session of; omitted by surfaces that don't carry it. */\n parentSessionId?: string | null;\n /** Sub-sessions of this row the viewer can see; omitted by surfaces that don't carry it. */\n subSessionCount?: number;\n /** Trigger marker (`manual` remains user-initiated; other values are automated). */\n triggerType: string | null;\n /** Last-message preview used when a legacy session has no useful title. */\n preview: string | null;\n messageCount: number | null;\n /** Per-user unread flag derived server-side from the read receipt. */\n isUnread: boolean;\n /**\n * Canonical session status (status_v2: idle | active | needs_input | error),\n * or null when the transport does not report it.\n */\n sessionStatus: string | null;\n /**\n * Canonical status of the session's current run (status_v2: scheduled |\n * queued | running | needs_input | completed | failed | canceled). Null when\n * the transport does not report it — {@link isConversationRunning} then\n * falls back to the legacy `state`. Running badges on every surface derive\n * from this one helper so they cannot disagree.\n */\n runStatus: string | null;\n /**\n * Server-derived attention state. `undefined` when the transport (or an\n * older server) does not report it — {@link deriveConversationAttention}\n * then falls back to the status planes; `null` when the server reports none.\n */\n attention?: ConversationAttention | null;\n /** One line (≤ 80 chars) of what the viewer should do next, when the server derives one. */\n nextAction?: string | null;\n /** Which signal produced `attention` / `nextAction`. */\n attentionSource?: ConversationAttentionSource | null;\n /** Whether the viewer pinned (favorited) the session; `undefined` when not reported. */\n isFavorited?: boolean;\n /**\n * Whether the viewer started the session (`false`: it is shared with them);\n * `undefined` when not reported. Never pushed by the conversation index, so\n * a fold keeps the fetched value.\n */\n createdByMe?: boolean;\n /** The last wake of a resting session; `undefined` when not reported. */\n lastWake?: ConversationWake | null;\n /**\n * Pull requests the session opened with their live GitHub state. `[]` when\n * it opened none, `null` when the state could not be read right now,\n * `undefined` when the transport did not ask for (or report) it.\n */\n pullRequests?: ConversationPullRequest[] | null;\n}\n\n/**\n * The canonical filter set. Semantics are defined here once; each fetcher\n * translates the fields its transport supports and throws on the rest.\n */\nexport interface ConversationListFilter {\n /** Exact-match client app filter; unset = all apps (SESSION_APP_ID_GUIDE). */\n appId?: string;\n /**\n * Sessions of any of these client apps too, a union with `appId` (REST thread\n * list only today). A host lists an earlier app's sessions beside its own.\n */\n appIds?: readonly string[];\n /**\n * The collab agent (`asset_<uuid>`) whose home this list's app is (REST\n * thread list only today, sent as `home_agent_id`). When the agent has a home\n * app set and the viewer can open the agent, the server widens the app scope\n * (`appId` / `appIds`) to every conversation with the agent too; every other\n * filter still applies. The page says whether it did\n * ({@link ConversationListPage.homeAgentApplied}); an older server ignores\n * the field and lists the app scope alone.\n */\n homeAgentId?: string;\n /** Asset types to include (e.g. ['session']); unset = all session types. */\n assetTypes?: readonly string[];\n /** Case-insensitive title search. */\n search?: string;\n /** Hide triggered/background sessions (REST thread list only today). */\n excludeTriggered?: boolean;\n /** Start-channel filter (v0 sessions / REST thread list only today). */\n sourceChannels?: readonly string[];\n /** Start channels to hide (e.g. ['api']); GraphQL `listConversations` only today. */\n excludeSourceChannels?: readonly string[];\n /** Exact computer asset whose sandbox started the session; GraphQL `listConversations` only today. */\n computerAssetId?: string;\n /** Only the sub-sessions of this session asset id; GraphQL `listConversations` only today. */\n parentSessionId?: string;\n /**\n * Lifecycle-state filter (e.g. ['running'], ['completed']). Transports match\n * it with the shared status_v2 rule (running carries a 12-hour recency cutoff).\n */\n states?: readonly string[];\n /** Restrict to sessions run by any of these agents (athena_metadata.agent). */\n agentIds?: readonly string[];\n /**\n * Include branched sub-sessions and orchestration children, which every list\n * hides by default because they belong under their parent thread.\n */\n includeSubSessions?: boolean;\n}\n\nexport interface ConversationListPage {\n /** Rows in contract order (updatedAt desc). */\n items: ConversationSummary[];\n /** Whether another `offset + limit` scan would return more rows. */\n hasMore: boolean;\n /** Total matching rows, or null when the transport does not count. */\n totalCount: number | null;\n /**\n * Whether the server widened this page to the conversations with the\n * filter's `homeAgentId` agent (REST `home_agent_applied`). Absent (an older\n * server, another transport, or no `homeAgentId`) means it did not.\n */\n homeAgentApplied?: boolean;\n}\n\nexport interface ConversationListFetchParams {\n limit: number;\n offset: number;\n filter: ConversationListFilter;\n signal?: AbortSignal;\n}\n\n/**\n * One page fetch against some transport (GraphQL `listConversations`, the\n * REST thread list, …). Must reject on filter fields it cannot express.\n */\nexport type ConversationListFetcher = (\n params: ConversationListFetchParams\n) => Promise<ConversationListPage>;\n\n/** Root of every conversation-list query key (all surfaces, all filters). */\nexport const CONVERSATION_LIST_QUERY_KEY_ROOT = 'athena-conversation-list';\n\n/**\n * Scope used by `useConversationList` when the caller passes no `scopeKey`.\n * Exported so cache seeders (e.g. Olympus's spacesBootstrap adoption) can\n * address the exact keys default-scoped hooks read from.\n */\nexport const CONVERSATION_LIST_DEFAULT_SCOPE_KEY = 'default';\n\n/**\n * Normalize a filter to its semantic content: unset/empty fields are dropped\n * (so `{}` and `{ appId: undefined }` are the same filter) and array order is\n * canonicalized. Fetchers receive the normalized form, so what a cache entry\n * claims to hold is exactly what was fetched.\n */\nexport function normalizeConversationListFilter(\n filter: ConversationListFilter\n): ConversationListFilter {\n const normalized: ConversationListFilter = {};\n if (filter.appId) normalized.appId = filter.appId;\n if (filter.appIds && filter.appIds.length > 0) {\n normalized.appIds = [...new Set(filter.appIds)].sort();\n }\n if (filter.homeAgentId) normalized.homeAgentId = filter.homeAgentId;\n if (filter.assetTypes && filter.assetTypes.length > 0) {\n normalized.assetTypes = [...filter.assetTypes].sort();\n }\n if (filter.search) normalized.search = filter.search;\n if (filter.excludeTriggered !== undefined) normalized.excludeTriggered = filter.excludeTriggered;\n if (filter.sourceChannels && filter.sourceChannels.length > 0) {\n normalized.sourceChannels = [...filter.sourceChannels].sort();\n }\n if (filter.excludeSourceChannels && filter.excludeSourceChannels.length > 0) {\n normalized.excludeSourceChannels = [...filter.excludeSourceChannels].sort();\n }\n if (filter.computerAssetId) normalized.computerAssetId = filter.computerAssetId;\n if (filter.parentSessionId) normalized.parentSessionId = filter.parentSessionId;\n if (filter.states && filter.states.length > 0) {\n normalized.states = [...filter.states].sort();\n }\n if (filter.agentIds && filter.agentIds.length > 0) {\n normalized.agentIds = [...filter.agentIds].sort();\n }\n if (filter.includeSubSessions) normalized.includeSubSessions = true;\n return normalized;\n}\n\n/** status_v2 run statuses during which a session is actively working. */\nconst RUNNING_RUN_STATUSES: ReadonlySet<string> = new Set(['queued', 'running']);\n\n/**\n * THE running rule for every conversation-list surface (sidebar pane, recent\n * dropdown, workspace section, pushed index rows): the canonical status_v2 run\n * status when the transport reports one, else the legacy `state`. A session\n * paused on a human gate (`needs_input`) is not running.\n */\nexport function isConversationRunning(summary: {\n runStatus?: string | null;\n state?: string | null;\n}): boolean {\n if (summary.runStatus !== null && summary.runStatus !== undefined) {\n return RUNNING_RUN_STATUSES.has(summary.runStatus);\n }\n return summary.state === 'running';\n}\n\nconst KNOWN_ATTENTIONS: ReadonlySet<string> = new Set(CONVERSATION_ATTENTIONS);\n\n/**\n * THE attention rule for every session-list surface: the server-derived\n * `attention` when the transport reports one it knows, else a client fallback\n * from the status planes so an older server still yields the two states a\n * list cannot do without — a human gate reads as `needs_you`, a live run as\n * `working`, a failed run as `error`. `null` when nothing applies.\n */\nexport function deriveConversationAttention(\n summary: Pick<ConversationSummary, 'attention' | 'runStatus' | 'sessionStatus' | 'state'>\n): ConversationAttention | null {\n if (typeof summary.attention === 'string' && KNOWN_ATTENTIONS.has(summary.attention)) {\n return summary.attention;\n }\n if (summary.runStatus === 'needs_input' || summary.sessionStatus === 'needs_input') {\n return 'needs_you';\n }\n if (isConversationRunning(summary)) return 'working';\n if (summary.runStatus === 'failed' || summary.sessionStatus === 'error') return 'error';\n return null;\n}\n\n/**\n * Stable cache identity for a filter: the normalized fields serialized in\n * sorted key order, so semantically equal filters share one cache entry.\n */\nexport function conversationListFilterKey(filter: ConversationListFilter): string {\n const normalized = normalizeConversationListFilter(filter) as Record<string, unknown>;\n return JSON.stringify(\n Object.keys(normalized)\n .sort()\n .map((key) => [key, normalized[key]])\n );\n}\n\n/**\n * Query keys for the two-tier pager. `scopeKey` isolates caches beyond the\n * filter — surfaces MUST include everything that changes whose list this is\n * (principal/user id, backend base URL) so a credential swap can never read\n * another identity's rows out of the cache.\n *\n * `pageSize` is part of the key because it is baked into every cached page's\n * offset boundaries: two hooks sharing a scope and filter but paging by\n * different sizes would otherwise reuse each other's incompatible boundaries\n * and skip or duplicate rows.\n */\nexport const conversationListQueryKeys = {\n root: [CONVERSATION_LIST_QUERY_KEY_ROOT] as const,\n scope: (scopeKey: string) => [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey] as const,\n firstPage: (scopeKey: string, filterKey: string, pageSize: number) =>\n [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey, filterKey, pageSize, 'first-page'] as const,\n history: (scopeKey: string, filterKey: string, pageSize: number) =>\n [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey, filterKey, pageSize, 'history'] as const,\n};\n\n/**\n * Flatten pages into one newest-first list, deduplicating by `threadId`.\n * The live first page wins over retained history copies of the same thread.\n */\nexport function mergeConversationPages(\n pages: readonly ConversationListPage[]\n): ConversationSummary[] {\n const seen = new Set<string>();\n const items: ConversationSummary[] = [];\n for (const page of pages) {\n for (const item of page.items) {\n if (seen.has(item.threadId)) continue;\n seen.add(item.threadId);\n items.push(item);\n }\n }\n return items;\n}\n\n/**\n * Prepend a conversation to a page unless its thread is already present\n * (optimistic \"new thread appears at the top\" write). Identity-preserving\n * no-op when present, so equal writes never notify subscribers.\n */\nexport function prependConversationToPage(\n page: ConversationListPage,\n item: ConversationSummary\n): ConversationListPage {\n if (page.items.some((existing) => existing.threadId === item.threadId)) return page;\n return {\n ...page,\n items: [item, ...page.items],\n totalCount: page.totalCount === null ? null : page.totalCount + 1,\n };\n}\n\n/** Remove a thread's row from a page; identity-preserving when absent. */\nexport function removeConversationFromPage(\n page: ConversationListPage,\n threadId: string\n): ConversationListPage {\n if (!page.items.some((item) => item.threadId === threadId)) return page;\n return { ...page, items: page.items.filter((item) => item.threadId !== threadId) };\n}\n\n/**\n * Patch every row matching `match`; identity-preserving when nothing matches\n * so cache writes cannot cause spurious re-renders.\n */\nexport function patchConversationsInPage(\n page: ConversationListPage,\n match: (item: ConversationSummary) => boolean,\n patch: Partial<ConversationSummary>\n): ConversationListPage {\n let changed = false;\n const items = page.items.map((item) => {\n if (!match(item)) return item;\n changed = true;\n return { ...item, ...patch };\n });\n return changed ? { ...page, items } : page;\n}\n\ntype CachedConversationList =\n | ConversationListPage\n | InfiniteData<ConversationListPage, number>\n | undefined;\n\nfunction isInfinitePages(\n data: ConversationListPage | InfiniteData<ConversationListPage, number>\n): data is InfiniteData<ConversationListPage, number> {\n return 'pages' in data;\n}\n\nfunction mapCachedPages(\n data: CachedConversationList,\n transform: (page: ConversationListPage) => ConversationListPage\n): CachedConversationList {\n if (!data) return data;\n if (isInfinitePages(data)) {\n let changed = false;\n const pages = data.pages.map((page) => {\n const next = transform(page);\n if (next !== page) changed = true;\n return next;\n });\n return changed ? { ...data, pages } : data;\n }\n return transform(data);\n}\n\ninterface ConversationListCacheScope {\n /** Restrict to one scope; omitted = every conversation-list cache. */\n scopeKey?: string;\n}\n\nfunction scopedKey(options?: ConversationListCacheScope): readonly string[] {\n return options?.scopeKey !== undefined\n ? conversationListQueryKeys.scope(options.scopeKey)\n : conversationListQueryKeys.root;\n}\n\n/**\n * Invalidate conversation-list caches (all filters, first page + history).\n * This is the hook for push signals — the conversation-index statewire, a\n * finished run, an archive/restore — anything that changed the server list.\n */\nexport function invalidateConversationListCaches(\n queryClient: QueryClient,\n options?: ConversationListCacheScope\n): Promise<void> {\n return queryClient.invalidateQueries({ queryKey: scopedKey(options) });\n}\n\n/** Cancel in-flight list fetches so an optimistic write cannot be overwritten. */\nexport function cancelConversationListCaches(\n queryClient: QueryClient,\n options?: ConversationListCacheScope\n): Promise<void> {\n return queryClient.cancelQueries({ queryKey: scopedKey(options) });\n}\n\n/**\n * Patch matching rows in every cached conversation list (first pages, history\n * pages, searches) so per-row state (read receipts, titles) flips in the same\n * frame as the interaction instead of after the next refetch.\n */\nexport function patchConversationListCaches(\n queryClient: QueryClient,\n options: ConversationListCacheScope & {\n match: (item: ConversationSummary) => boolean;\n patch: Partial<ConversationSummary>;\n }\n): void {\n queryClient.setQueriesData<CachedConversationList>({ queryKey: scopedKey(options) }, (data) =>\n mapCachedPages(data, (page) => patchConversationsInPage(page, options.match, options.patch))\n );\n}\n\n/** Remove a thread's row from every cached conversation list in scope. */\nexport function removeConversationFromListCaches(\n queryClient: QueryClient,\n options: ConversationListCacheScope & { threadId: string }\n): void {\n queryClient.setQueriesData<CachedConversationList>({ queryKey: scopedKey(options) }, (data) =>\n mapCachedPages(data, (page) => removeConversationFromPage(page, options.threadId))\n );\n}\n\nfunction timestampMs(value: string): number {\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? 0 : parsed;\n}\n\nfunction sameSummary(a: ConversationSummary, b: ConversationSummary): boolean {\n return (\n a.threadId === b.threadId &&\n a.title === b.title &&\n a.updatedAt === b.updatedAt &&\n a.createdAt === b.createdAt &&\n a.state === b.state &&\n a.runtime === b.runtime &&\n a.sourceChannel === b.sourceChannel &&\n a.assetType === b.assetType &&\n a.assetId === b.assetId &&\n a.appId === b.appId &&\n a.triggerType === b.triggerType &&\n a.preview === b.preview &&\n a.messageCount === b.messageCount &&\n a.isUnread === b.isUnread &&\n a.sessionStatus === b.sessionStatus &&\n a.runStatus === b.runStatus &&\n a.attention === b.attention &&\n a.nextAction === b.nextAction &&\n a.attentionSource === b.attentionSource &&\n a.isFavorited === b.isFavorited &&\n // A transport that does not carry the agent (`undefined`) and a push that\n // says the session has none (`null`) agree.\n (a.collabAgentId ?? null) === (b.collabAgentId ?? null) &&\n // Never pushed by the index, so a fold carries the previous references\n // through unchanged; identity is the equality that matters here.\n a.lastWake === b.lastWake &&\n a.pullRequests === b.pullRequests\n );\n}\n\n/**\n * Project one replicated conversation-index row onto the shared summary shape.\n * Fields the index never carries (`createdAt`) and viewer-specific fields a\n * lifecycle publish cannot know (`is_unread: null`) keep the row's previous\n * value. Canonical status is taken from the entry when the publishing pod\n * reports it; a snapshot from a pod that predates the fields falls back to the\n * legacy `state` (null status) rather than keeping a stale run status.\n *\n * Attention fields (`attention`, `next_action`, `attention_source`,\n * `is_favorited`) are taken from the entry when the publishing pod reports\n * them, so a live push flips a row to \"needs you\" the way it renames it; a\n * pod that predates them leaves the row's previous values. The collab agent\n * (`collab_agent_id`) is taken the same way. Pull-request state, the last wake\n * and `createdByMe` are never pushed and always keep the fetched value.\n */\nexport function summaryFromConversationIndexEntry(\n entry: ConversationIndexEntry,\n previous?: ConversationSummary\n): ConversationSummary {\n const carriesStatus = entry.run_status !== undefined || entry.session_status !== undefined;\n return {\n threadId: entry.thread_id,\n title: entry.title,\n updatedAt: entry.updated_at,\n createdAt: previous?.createdAt ?? null,\n state: entry.state,\n runtime: entry.runtime,\n sourceChannel: entry.start_channel,\n assetType: entry.asset_type,\n assetId: entry.asset_id ?? previous?.assetId ?? null,\n computerAssetId: previous?.computerAssetId ?? null,\n collabAgentId:\n entry.collab_agent_id !== undefined ? entry.collab_agent_id : previous?.collabAgentId,\n collabChannelId: previous?.collabChannelId,\n parentSessionId: previous?.parentSessionId,\n subSessionCount: previous?.subSessionCount,\n appId: entry.app_id,\n triggerType: entry.trigger_type,\n preview: entry.last_message_preview,\n messageCount: entry.message_count,\n isUnread: entry.is_unread ?? previous?.isUnread ?? false,\n sessionStatus: carriesStatus ? (entry.session_status ?? null) : null,\n runStatus: carriesStatus ? (entry.run_status ?? null) : null,\n attention: entry.attention !== undefined ? entry.attention : previous?.attention,\n nextAction: entry.next_action !== undefined ? entry.next_action : previous?.nextAction,\n attentionSource:\n entry.attention_source !== undefined ? entry.attention_source : previous?.attentionSource,\n isFavorited: entry.is_favorited ?? previous?.isFavorited,\n createdByMe: previous?.createdByMe,\n lastWake: previous?.lastWake,\n pullRequests: previous?.pullRequests,\n };\n}\n\n/**\n * Fold the user's replicated conversation index (the newest-page window the\n * per-user statewire pushes) into a cached live first page, so a thread\n * created, renamed, finished, or read anywhere updates every list surface in\n * the same frame — without refetching the page.\n *\n * - Rows in the window are upserted (new threads appear, existing rows take\n * the pushed fields, see {@link summaryFromConversationIndexEntry}).\n * - Rows NOT in the window but at least as new as the window's oldest row\n * should have been in it, so they left the list (archived, hidden, shared\n * away) and are dropped. Older rows are beyond the window and are kept.\n * - When the index reports no further rows (`indexHasMore === false`) the\n * window IS the whole list, so every row outside it is dropped.\n *\n * Newest-first ordering is restored and the page identity is preserved when\n * nothing changed, so an equal push never notifies subscribers.\n */\nexport function mergeConversationIndexIntoPage(\n page: ConversationListPage,\n entries: readonly ConversationIndexEntry[],\n options: { indexHasMore: boolean }\n): ConversationListPage {\n const previousByThread = new Map(page.items.map((item) => [item.threadId, item]));\n const inWindow = new Set(entries.map((entry) => entry.thread_id));\n const windowTailMs =\n entries.length > 0 ? Math.min(...entries.map((entry) => timestampMs(entry.updated_at))) : null;\n\n const merged: ConversationSummary[] = entries.map((entry) => {\n const previous = previousByThread.get(entry.thread_id);\n const next = summaryFromConversationIndexEntry(entry, previous);\n return previous && sameSummary(previous, next) ? previous : next;\n });\n\n for (const item of page.items) {\n if (inWindow.has(item.threadId)) continue;\n if (!options.indexHasMore) continue;\n if (windowTailMs !== null && timestampMs(item.updatedAt) >= windowTailMs) continue;\n merged.push(item);\n }\n\n merged.sort((a, b) => timestampMs(b.updatedAt) - timestampMs(a.updatedAt));\n\n const unchanged =\n merged.length === page.items.length &&\n merged.every((item, index) => item === page.items[index]);\n if (unchanged) return page;\n\n return { ...page, items: merged };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -160,6 +160,16 @@ export interface ConversationListFilter {
|
|
|
160
160
|
* list only today). A host lists an earlier app's sessions beside its own.
|
|
161
161
|
*/
|
|
162
162
|
appIds?: readonly string[];
|
|
163
|
+
/**
|
|
164
|
+
* The collab agent (`asset_<uuid>`) whose home this list's app is (REST
|
|
165
|
+
* thread list only today, sent as `home_agent_id`). When the agent has a home
|
|
166
|
+
* app set and the viewer can open the agent, the server widens the app scope
|
|
167
|
+
* (`appId` / `appIds`) to every conversation with the agent too; every other
|
|
168
|
+
* filter still applies. The page says whether it did
|
|
169
|
+
* ({@link ConversationListPage.homeAgentApplied}); an older server ignores
|
|
170
|
+
* the field and lists the app scope alone.
|
|
171
|
+
*/
|
|
172
|
+
homeAgentId?: string;
|
|
163
173
|
/** Asset types to include (e.g. ['session']); unset = all session types. */
|
|
164
174
|
assetTypes?: readonly string[];
|
|
165
175
|
/** Case-insensitive title search. */
|
|
@@ -194,6 +204,12 @@ export interface ConversationListPage {
|
|
|
194
204
|
hasMore: boolean;
|
|
195
205
|
/** Total matching rows, or null when the transport does not count. */
|
|
196
206
|
totalCount: number | null;
|
|
207
|
+
/**
|
|
208
|
+
* Whether the server widened this page to the conversations with the
|
|
209
|
+
* filter's `homeAgentId` agent (REST `home_agent_applied`). Absent (an older
|
|
210
|
+
* server, another transport, or no `homeAgentId`) means it did not.
|
|
211
|
+
*/
|
|
212
|
+
homeAgentApplied?: boolean;
|
|
197
213
|
}
|
|
198
214
|
export interface ConversationListFetchParams {
|
|
199
215
|
limit: number;
|
|
@@ -315,9 +331,9 @@ export declare function removeConversationFromListCaches(queryClient: QueryClien
|
|
|
315
331
|
* Attention fields (`attention`, `next_action`, `attention_source`,
|
|
316
332
|
* `is_favorited`) are taken from the entry when the publishing pod reports
|
|
317
333
|
* them, so a live push flips a row to "needs you" the way it renames it; a
|
|
318
|
-
* pod that predates them leaves the row's previous values.
|
|
319
|
-
*
|
|
320
|
-
* value.
|
|
334
|
+
* pod that predates them leaves the row's previous values. The collab agent
|
|
335
|
+
* (`collab_agent_id`) is taken the same way. Pull-request state, the last wake
|
|
336
|
+
* and `createdByMe` are never pushed and always keep the fetched value.
|
|
321
337
|
*/
|
|
322
338
|
export declare function summaryFromConversationIndexEntry(entry: ConversationIndexEntry, previous?: ConversationSummary): ConversationSummary;
|
|
323
339
|
/**
|
|
@@ -14,6 +14,7 @@ function normalizeConversationListFilter(filter) {
|
|
|
14
14
|
if (filter.appIds && filter.appIds.length > 0) {
|
|
15
15
|
normalized.appIds = [...new Set(filter.appIds)].sort();
|
|
16
16
|
}
|
|
17
|
+
if (filter.homeAgentId) normalized.homeAgentId = filter.homeAgentId;
|
|
17
18
|
if (filter.assetTypes && filter.assetTypes.length > 0) {
|
|
18
19
|
normalized.assetTypes = [...filter.assetTypes].sort();
|
|
19
20
|
}
|
|
@@ -147,7 +148,7 @@ function summaryFromConversationIndexEntry(entry, previous) {
|
|
|
147
148
|
assetType: entry.asset_type,
|
|
148
149
|
assetId: entry.asset_id ?? (previous == null ? void 0 : previous.assetId) ?? null,
|
|
149
150
|
computerAssetId: (previous == null ? void 0 : previous.computerAssetId) ?? null,
|
|
150
|
-
collabAgentId: previous == null ? void 0 : previous.collabAgentId,
|
|
151
|
+
collabAgentId: entry.collab_agent_id !== void 0 ? entry.collab_agent_id : previous == null ? void 0 : previous.collabAgentId,
|
|
151
152
|
collabChannelId: previous == null ? void 0 : previous.collabChannelId,
|
|
152
153
|
parentSessionId: previous == null ? void 0 : previous.parentSessionId,
|
|
153
154
|
subSessionCount: previous == null ? void 0 : previous.subSessionCount,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation-list.js","sources":["../../../../agent-runtime/src/conversation-list.ts"],"sourcesContent":["import type { InfiniteData, QueryClient } from '@tanstack/react-query';\nimport type { ConversationIndexEntry } from './conversation-index';\n\n/**\n * What a session asks of the viewer, derived server-side from the run, the\n * parked interrupt, the pull requests it opened, its termination and its last\n * wake (the Devin-style session list). `needs_you` is the only state that\n * demands the viewer act; `working` is live; `ready` finished with something\n * to look at; `watching` waits on an external party (a review, a check);\n * `done` and `error` are terminal. A backend newer than this client may report\n * a value outside the union — render it as \"no attention\" rather than failing.\n */\nexport type ConversationAttention =\n | 'needs_you'\n | 'working'\n | 'ready'\n | 'watching'\n | 'done'\n | 'error';\n\nexport const CONVERSATION_ATTENTIONS: readonly ConversationAttention[] = [\n 'needs_you',\n 'working',\n 'ready',\n 'watching',\n 'done',\n 'error',\n];\n\n/** Which signal produced the attention state (and the `nextAction` copy). */\nexport type ConversationAttentionSource =\n | 'interrupt'\n | 'run'\n | 'pull_request'\n | 'termination'\n | 'wake'\n | 'handoff'\n | 'follow_up';\n\n/** The last event that woke a resting session (a GitHub webhook, a schedule, a handoff). */\nexport interface ConversationWake {\n source: string | null;\n reason: string | null;\n /** ISO 8601 timestamp of the wake. */\n at: string | null;\n}\n\n/**\n * Live GitHub state of a pull request the session opened, read by the server\n * when the list renders. Never pushed by the conversation index — it comes\n * from the list fetch only, so a fold keeps the fetched value.\n */\nexport interface ConversationPullRequest {\n /** `owner/repo` the pull request lives in. */\n repoFullName: string;\n number: number;\n url: string;\n title: string;\n state: 'OPEN' | 'CLOSED' | 'MERGED';\n isDraft: boolean;\n /** GitHub's review decision (APPROVED, CHANGES_REQUESTED, REVIEW_REQUIRED) or null. */\n reviewDecision: string | null;\n /** Combined state of the checks on the head commit. */\n checksState: 'SUCCESS' | 'FAILURE' | 'PENDING' | 'NONE';\n /** ISO 8601 timestamp of the pull request's last update on GitHub. */\n updatedAt: string;\n}\n\n/**\n * Shared conversation/thread LIST contract (chat-stack unification —\n * thread-list-contract-convergence).\n *\n * Every chat surface lists \"my conversations\" against the same underlying\n * session assets, but historically each declared its own row shape, paging\n * model, and filter set (Olympus GraphQL `listConversations`, the SDK/Chrome\n * REST `/api/conversations/threads/list`, mobile's `/api/v0/sessions`). This\n * module is the ONE contract those surfaces converge on:\n *\n * - **Row shape**: {@link ConversationSummary} — the canonical camelCase\n * superset. Fields a transport cannot supply are `null`, never invented.\n * - **Ordering**: newest first by `updatedAt`, always. Ordering is part of the\n * contract, not a parameter — no surface sorts any other way.\n * - **Paging**: offset/limit with a LIVE newest page and explicitly-fetched\n * frozen history pages (see `useConversationList`). `hasMore` continues the\n * scan; `totalCount` is advisory and may be `null` for transports that do\n * not count.\n * - **Filtering**: {@link ConversationListFilter}. `appId` follows\n * `docs/SESSION_APP_ID_GUIDE.md`: exact match when set (legacy `null`\n * sessions excluded), no filter when unset. A fetcher that cannot express a\n * requested filter field MUST throw rather than silently ignore it — a\n * dropped filter returns wrong rows under a cache key that claims otherwise.\n * - **Freshness**: the per-user conversation-index statewire\n * (`conversation-index.ts`) is the push signal; these queries are the data\n * source. Push handlers call {@link invalidateConversationListCaches}.\n */\nexport interface ConversationSummary {\n /** LangGraph thread id — the cross-surface conversation identity. */\n threadId: string;\n title: string;\n /** ISO 8601 timestamp of last update (the contract's sort key). */\n updatedAt: string;\n /** ISO 8601 creation timestamp, when the transport reports it. */\n createdAt: string | null;\n /** Session lifecycle state from athena_metadata (e.g. running, completed). */\n state: string | null;\n /** Agent runtime marker (e.g. 'athena_deep_agent'), when reported. */\n runtime: string | null;\n /** Channel the session was started from (e.g. agent_email, web). */\n sourceChannel: string | null;\n /** Asset type (session, video_session, desktop_session, mobile_session). */\n assetType: string | null;\n /** Session asset id (`asset_<uuid>`), when the transport reports it. */\n assetId: string | null;\n /** Client app that created the session (SESSION_APP_ID_GUIDE). */\n appId: string | null;\n /** Computer asset whose sandbox started the session (`sourceChannel` 'computer'); omitted by surfaces that don't carry it. */\n computerAssetId?: string | null;\n /**\n * Collab agent (`asset_<uuid>`) and channel the session started on; `null`\n * channel is the agent's default channel. The server refuses a run on another\n * of the agent's channels, so a client shows the session's mode from these\n * instead of remembering it. Omitted by surfaces that don't carry them.\n */\n collabAgentId?: string | null;\n collabChannelId?: string | null;\n /** Session asset id this row is a sub-session of; omitted by surfaces that don't carry it. */\n parentSessionId?: string | null;\n /** Sub-sessions of this row the viewer can see; omitted by surfaces that don't carry it. */\n subSessionCount?: number;\n /** Trigger marker (`manual` remains user-initiated; other values are automated). */\n triggerType: string | null;\n /** Last-message preview used when a legacy session has no useful title. */\n preview: string | null;\n messageCount: number | null;\n /** Per-user unread flag derived server-side from the read receipt. */\n isUnread: boolean;\n /**\n * Canonical session status (status_v2: idle | active | needs_input | error),\n * or null when the transport does not report it.\n */\n sessionStatus: string | null;\n /**\n * Canonical status of the session's current run (status_v2: scheduled |\n * queued | running | needs_input | completed | failed | canceled). Null when\n * the transport does not report it — {@link isConversationRunning} then\n * falls back to the legacy `state`. Running badges on every surface derive\n * from this one helper so they cannot disagree.\n */\n runStatus: string | null;\n /**\n * Server-derived attention state. `undefined` when the transport (or an\n * older server) does not report it — {@link deriveConversationAttention}\n * then falls back to the status planes; `null` when the server reports none.\n */\n attention?: ConversationAttention | null;\n /** One line (≤ 80 chars) of what the viewer should do next, when the server derives one. */\n nextAction?: string | null;\n /** Which signal produced `attention` / `nextAction`. */\n attentionSource?: ConversationAttentionSource | null;\n /** Whether the viewer pinned (favorited) the session; `undefined` when not reported. */\n isFavorited?: boolean;\n /**\n * Whether the viewer started the session (`false`: it is shared with them);\n * `undefined` when not reported. Never pushed by the conversation index, so\n * a fold keeps the fetched value.\n */\n createdByMe?: boolean;\n /** The last wake of a resting session; `undefined` when not reported. */\n lastWake?: ConversationWake | null;\n /**\n * Pull requests the session opened with their live GitHub state. `[]` when\n * it opened none, `null` when the state could not be read right now,\n * `undefined` when the transport did not ask for (or report) it.\n */\n pullRequests?: ConversationPullRequest[] | null;\n}\n\n/**\n * The canonical filter set. Semantics are defined here once; each fetcher\n * translates the fields its transport supports and throws on the rest.\n */\nexport interface ConversationListFilter {\n /** Exact-match client app filter; unset = all apps (SESSION_APP_ID_GUIDE). */\n appId?: string;\n /**\n * Sessions of any of these client apps too, a union with `appId` (REST thread\n * list only today). A host lists an earlier app's sessions beside its own.\n */\n appIds?: readonly string[];\n /** Asset types to include (e.g. ['session']); unset = all session types. */\n assetTypes?: readonly string[];\n /** Case-insensitive title search. */\n search?: string;\n /** Hide triggered/background sessions (REST thread list only today). */\n excludeTriggered?: boolean;\n /** Start-channel filter (v0 sessions / REST thread list only today). */\n sourceChannels?: readonly string[];\n /** Start channels to hide (e.g. ['api']); GraphQL `listConversations` only today. */\n excludeSourceChannels?: readonly string[];\n /** Exact computer asset whose sandbox started the session; GraphQL `listConversations` only today. */\n computerAssetId?: string;\n /** Only the sub-sessions of this session asset id; GraphQL `listConversations` only today. */\n parentSessionId?: string;\n /**\n * Lifecycle-state filter (e.g. ['running'], ['completed']). Transports match\n * it with the shared status_v2 rule (running carries a 12-hour recency cutoff).\n */\n states?: readonly string[];\n /** Restrict to sessions run by any of these agents (athena_metadata.agent). */\n agentIds?: readonly string[];\n /**\n * Include branched sub-sessions and orchestration children, which every list\n * hides by default because they belong under their parent thread.\n */\n includeSubSessions?: boolean;\n}\n\nexport interface ConversationListPage {\n /** Rows in contract order (updatedAt desc). */\n items: ConversationSummary[];\n /** Whether another `offset + limit` scan would return more rows. */\n hasMore: boolean;\n /** Total matching rows, or null when the transport does not count. */\n totalCount: number | null;\n}\n\nexport interface ConversationListFetchParams {\n limit: number;\n offset: number;\n filter: ConversationListFilter;\n signal?: AbortSignal;\n}\n\n/**\n * One page fetch against some transport (GraphQL `listConversations`, the\n * REST thread list, …). Must reject on filter fields it cannot express.\n */\nexport type ConversationListFetcher = (\n params: ConversationListFetchParams\n) => Promise<ConversationListPage>;\n\n/** Root of every conversation-list query key (all surfaces, all filters). */\nexport const CONVERSATION_LIST_QUERY_KEY_ROOT = 'athena-conversation-list';\n\n/**\n * Scope used by `useConversationList` when the caller passes no `scopeKey`.\n * Exported so cache seeders (e.g. Olympus's spacesBootstrap adoption) can\n * address the exact keys default-scoped hooks read from.\n */\nexport const CONVERSATION_LIST_DEFAULT_SCOPE_KEY = 'default';\n\n/**\n * Normalize a filter to its semantic content: unset/empty fields are dropped\n * (so `{}` and `{ appId: undefined }` are the same filter) and array order is\n * canonicalized. Fetchers receive the normalized form, so what a cache entry\n * claims to hold is exactly what was fetched.\n */\nexport function normalizeConversationListFilter(\n filter: ConversationListFilter\n): ConversationListFilter {\n const normalized: ConversationListFilter = {};\n if (filter.appId) normalized.appId = filter.appId;\n if (filter.appIds && filter.appIds.length > 0) {\n normalized.appIds = [...new Set(filter.appIds)].sort();\n }\n if (filter.assetTypes && filter.assetTypes.length > 0) {\n normalized.assetTypes = [...filter.assetTypes].sort();\n }\n if (filter.search) normalized.search = filter.search;\n if (filter.excludeTriggered !== undefined) normalized.excludeTriggered = filter.excludeTriggered;\n if (filter.sourceChannels && filter.sourceChannels.length > 0) {\n normalized.sourceChannels = [...filter.sourceChannels].sort();\n }\n if (filter.excludeSourceChannels && filter.excludeSourceChannels.length > 0) {\n normalized.excludeSourceChannels = [...filter.excludeSourceChannels].sort();\n }\n if (filter.computerAssetId) normalized.computerAssetId = filter.computerAssetId;\n if (filter.parentSessionId) normalized.parentSessionId = filter.parentSessionId;\n if (filter.states && filter.states.length > 0) {\n normalized.states = [...filter.states].sort();\n }\n if (filter.agentIds && filter.agentIds.length > 0) {\n normalized.agentIds = [...filter.agentIds].sort();\n }\n if (filter.includeSubSessions) normalized.includeSubSessions = true;\n return normalized;\n}\n\n/** status_v2 run statuses during which a session is actively working. */\nconst RUNNING_RUN_STATUSES: ReadonlySet<string> = new Set(['queued', 'running']);\n\n/**\n * THE running rule for every conversation-list surface (sidebar pane, recent\n * dropdown, workspace section, pushed index rows): the canonical status_v2 run\n * status when the transport reports one, else the legacy `state`. A session\n * paused on a human gate (`needs_input`) is not running.\n */\nexport function isConversationRunning(summary: {\n runStatus?: string | null;\n state?: string | null;\n}): boolean {\n if (summary.runStatus !== null && summary.runStatus !== undefined) {\n return RUNNING_RUN_STATUSES.has(summary.runStatus);\n }\n return summary.state === 'running';\n}\n\nconst KNOWN_ATTENTIONS: ReadonlySet<string> = new Set(CONVERSATION_ATTENTIONS);\n\n/**\n * THE attention rule for every session-list surface: the server-derived\n * `attention` when the transport reports one it knows, else a client fallback\n * from the status planes so an older server still yields the two states a\n * list cannot do without — a human gate reads as `needs_you`, a live run as\n * `working`, a failed run as `error`. `null` when nothing applies.\n */\nexport function deriveConversationAttention(\n summary: Pick<ConversationSummary, 'attention' | 'runStatus' | 'sessionStatus' | 'state'>\n): ConversationAttention | null {\n if (typeof summary.attention === 'string' && KNOWN_ATTENTIONS.has(summary.attention)) {\n return summary.attention;\n }\n if (summary.runStatus === 'needs_input' || summary.sessionStatus === 'needs_input') {\n return 'needs_you';\n }\n if (isConversationRunning(summary)) return 'working';\n if (summary.runStatus === 'failed' || summary.sessionStatus === 'error') return 'error';\n return null;\n}\n\n/**\n * Stable cache identity for a filter: the normalized fields serialized in\n * sorted key order, so semantically equal filters share one cache entry.\n */\nexport function conversationListFilterKey(filter: ConversationListFilter): string {\n const normalized = normalizeConversationListFilter(filter) as Record<string, unknown>;\n return JSON.stringify(\n Object.keys(normalized)\n .sort()\n .map((key) => [key, normalized[key]])\n );\n}\n\n/**\n * Query keys for the two-tier pager. `scopeKey` isolates caches beyond the\n * filter — surfaces MUST include everything that changes whose list this is\n * (principal/user id, backend base URL) so a credential swap can never read\n * another identity's rows out of the cache.\n *\n * `pageSize` is part of the key because it is baked into every cached page's\n * offset boundaries: two hooks sharing a scope and filter but paging by\n * different sizes would otherwise reuse each other's incompatible boundaries\n * and skip or duplicate rows.\n */\nexport const conversationListQueryKeys = {\n root: [CONVERSATION_LIST_QUERY_KEY_ROOT] as const,\n scope: (scopeKey: string) => [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey] as const,\n firstPage: (scopeKey: string, filterKey: string, pageSize: number) =>\n [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey, filterKey, pageSize, 'first-page'] as const,\n history: (scopeKey: string, filterKey: string, pageSize: number) =>\n [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey, filterKey, pageSize, 'history'] as const,\n};\n\n/**\n * Flatten pages into one newest-first list, deduplicating by `threadId`.\n * The live first page wins over retained history copies of the same thread.\n */\nexport function mergeConversationPages(\n pages: readonly ConversationListPage[]\n): ConversationSummary[] {\n const seen = new Set<string>();\n const items: ConversationSummary[] = [];\n for (const page of pages) {\n for (const item of page.items) {\n if (seen.has(item.threadId)) continue;\n seen.add(item.threadId);\n items.push(item);\n }\n }\n return items;\n}\n\n/**\n * Prepend a conversation to a page unless its thread is already present\n * (optimistic \"new thread appears at the top\" write). Identity-preserving\n * no-op when present, so equal writes never notify subscribers.\n */\nexport function prependConversationToPage(\n page: ConversationListPage,\n item: ConversationSummary\n): ConversationListPage {\n if (page.items.some((existing) => existing.threadId === item.threadId)) return page;\n return {\n ...page,\n items: [item, ...page.items],\n totalCount: page.totalCount === null ? null : page.totalCount + 1,\n };\n}\n\n/** Remove a thread's row from a page; identity-preserving when absent. */\nexport function removeConversationFromPage(\n page: ConversationListPage,\n threadId: string\n): ConversationListPage {\n if (!page.items.some((item) => item.threadId === threadId)) return page;\n return { ...page, items: page.items.filter((item) => item.threadId !== threadId) };\n}\n\n/**\n * Patch every row matching `match`; identity-preserving when nothing matches\n * so cache writes cannot cause spurious re-renders.\n */\nexport function patchConversationsInPage(\n page: ConversationListPage,\n match: (item: ConversationSummary) => boolean,\n patch: Partial<ConversationSummary>\n): ConversationListPage {\n let changed = false;\n const items = page.items.map((item) => {\n if (!match(item)) return item;\n changed = true;\n return { ...item, ...patch };\n });\n return changed ? { ...page, items } : page;\n}\n\ntype CachedConversationList =\n | ConversationListPage\n | InfiniteData<ConversationListPage, number>\n | undefined;\n\nfunction isInfinitePages(\n data: ConversationListPage | InfiniteData<ConversationListPage, number>\n): data is InfiniteData<ConversationListPage, number> {\n return 'pages' in data;\n}\n\nfunction mapCachedPages(\n data: CachedConversationList,\n transform: (page: ConversationListPage) => ConversationListPage\n): CachedConversationList {\n if (!data) return data;\n if (isInfinitePages(data)) {\n let changed = false;\n const pages = data.pages.map((page) => {\n const next = transform(page);\n if (next !== page) changed = true;\n return next;\n });\n return changed ? { ...data, pages } : data;\n }\n return transform(data);\n}\n\ninterface ConversationListCacheScope {\n /** Restrict to one scope; omitted = every conversation-list cache. */\n scopeKey?: string;\n}\n\nfunction scopedKey(options?: ConversationListCacheScope): readonly string[] {\n return options?.scopeKey !== undefined\n ? conversationListQueryKeys.scope(options.scopeKey)\n : conversationListQueryKeys.root;\n}\n\n/**\n * Invalidate conversation-list caches (all filters, first page + history).\n * This is the hook for push signals — the conversation-index statewire, a\n * finished run, an archive/restore — anything that changed the server list.\n */\nexport function invalidateConversationListCaches(\n queryClient: QueryClient,\n options?: ConversationListCacheScope\n): Promise<void> {\n return queryClient.invalidateQueries({ queryKey: scopedKey(options) });\n}\n\n/** Cancel in-flight list fetches so an optimistic write cannot be overwritten. */\nexport function cancelConversationListCaches(\n queryClient: QueryClient,\n options?: ConversationListCacheScope\n): Promise<void> {\n return queryClient.cancelQueries({ queryKey: scopedKey(options) });\n}\n\n/**\n * Patch matching rows in every cached conversation list (first pages, history\n * pages, searches) so per-row state (read receipts, titles) flips in the same\n * frame as the interaction instead of after the next refetch.\n */\nexport function patchConversationListCaches(\n queryClient: QueryClient,\n options: ConversationListCacheScope & {\n match: (item: ConversationSummary) => boolean;\n patch: Partial<ConversationSummary>;\n }\n): void {\n queryClient.setQueriesData<CachedConversationList>({ queryKey: scopedKey(options) }, (data) =>\n mapCachedPages(data, (page) => patchConversationsInPage(page, options.match, options.patch))\n );\n}\n\n/** Remove a thread's row from every cached conversation list in scope. */\nexport function removeConversationFromListCaches(\n queryClient: QueryClient,\n options: ConversationListCacheScope & { threadId: string }\n): void {\n queryClient.setQueriesData<CachedConversationList>({ queryKey: scopedKey(options) }, (data) =>\n mapCachedPages(data, (page) => removeConversationFromPage(page, options.threadId))\n );\n}\n\nfunction timestampMs(value: string): number {\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? 0 : parsed;\n}\n\nfunction sameSummary(a: ConversationSummary, b: ConversationSummary): boolean {\n return (\n a.threadId === b.threadId &&\n a.title === b.title &&\n a.updatedAt === b.updatedAt &&\n a.createdAt === b.createdAt &&\n a.state === b.state &&\n a.runtime === b.runtime &&\n a.sourceChannel === b.sourceChannel &&\n a.assetType === b.assetType &&\n a.assetId === b.assetId &&\n a.appId === b.appId &&\n a.triggerType === b.triggerType &&\n a.preview === b.preview &&\n a.messageCount === b.messageCount &&\n a.isUnread === b.isUnread &&\n a.sessionStatus === b.sessionStatus &&\n a.runStatus === b.runStatus &&\n a.attention === b.attention &&\n a.nextAction === b.nextAction &&\n a.attentionSource === b.attentionSource &&\n a.isFavorited === b.isFavorited &&\n // Never pushed by the index, so a fold carries the previous references\n // through unchanged; identity is the equality that matters here.\n a.lastWake === b.lastWake &&\n a.pullRequests === b.pullRequests\n );\n}\n\n/**\n * Project one replicated conversation-index row onto the shared summary shape.\n * Fields the index never carries (`createdAt`) and viewer-specific fields a\n * lifecycle publish cannot know (`is_unread: null`) keep the row's previous\n * value. Canonical status is taken from the entry when the publishing pod\n * reports it; a snapshot from a pod that predates the fields falls back to the\n * legacy `state` (null status) rather than keeping a stale run status.\n *\n * Attention fields (`attention`, `next_action`, `attention_source`,\n * `is_favorited`) are taken from the entry when the publishing pod reports\n * them, so a live push flips a row to \"needs you\" the way it renames it; a\n * pod that predates them leaves the row's previous values. Pull-request state,\n * the last wake and `createdByMe` are never pushed and always keep the fetched\n * value.\n */\nexport function summaryFromConversationIndexEntry(\n entry: ConversationIndexEntry,\n previous?: ConversationSummary\n): ConversationSummary {\n const carriesStatus = entry.run_status !== undefined || entry.session_status !== undefined;\n return {\n threadId: entry.thread_id,\n title: entry.title,\n updatedAt: entry.updated_at,\n createdAt: previous?.createdAt ?? null,\n state: entry.state,\n runtime: entry.runtime,\n sourceChannel: entry.start_channel,\n assetType: entry.asset_type,\n assetId: entry.asset_id ?? previous?.assetId ?? null,\n computerAssetId: previous?.computerAssetId ?? null,\n collabAgentId: previous?.collabAgentId,\n collabChannelId: previous?.collabChannelId,\n parentSessionId: previous?.parentSessionId,\n subSessionCount: previous?.subSessionCount,\n appId: entry.app_id,\n triggerType: entry.trigger_type,\n preview: entry.last_message_preview,\n messageCount: entry.message_count,\n isUnread: entry.is_unread ?? previous?.isUnread ?? false,\n sessionStatus: carriesStatus ? (entry.session_status ?? null) : null,\n runStatus: carriesStatus ? (entry.run_status ?? null) : null,\n attention: entry.attention !== undefined ? entry.attention : previous?.attention,\n nextAction: entry.next_action !== undefined ? entry.next_action : previous?.nextAction,\n attentionSource:\n entry.attention_source !== undefined ? entry.attention_source : previous?.attentionSource,\n isFavorited: entry.is_favorited ?? previous?.isFavorited,\n createdByMe: previous?.createdByMe,\n lastWake: previous?.lastWake,\n pullRequests: previous?.pullRequests,\n };\n}\n\n/**\n * Fold the user's replicated conversation index (the newest-page window the\n * per-user statewire pushes) into a cached live first page, so a thread\n * created, renamed, finished, or read anywhere updates every list surface in\n * the same frame — without refetching the page.\n *\n * - Rows in the window are upserted (new threads appear, existing rows take\n * the pushed fields, see {@link summaryFromConversationIndexEntry}).\n * - Rows NOT in the window but at least as new as the window's oldest row\n * should have been in it, so they left the list (archived, hidden, shared\n * away) and are dropped. Older rows are beyond the window and are kept.\n * - When the index reports no further rows (`indexHasMore === false`) the\n * window IS the whole list, so every row outside it is dropped.\n *\n * Newest-first ordering is restored and the page identity is preserved when\n * nothing changed, so an equal push never notifies subscribers.\n */\nexport function mergeConversationIndexIntoPage(\n page: ConversationListPage,\n entries: readonly ConversationIndexEntry[],\n options: { indexHasMore: boolean }\n): ConversationListPage {\n const previousByThread = new Map(page.items.map((item) => [item.threadId, item]));\n const inWindow = new Set(entries.map((entry) => entry.thread_id));\n const windowTailMs =\n entries.length > 0 ? Math.min(...entries.map((entry) => timestampMs(entry.updated_at))) : null;\n\n const merged: ConversationSummary[] = entries.map((entry) => {\n const previous = previousByThread.get(entry.thread_id);\n const next = summaryFromConversationIndexEntry(entry, previous);\n return previous && sameSummary(previous, next) ? previous : next;\n });\n\n for (const item of page.items) {\n if (inWindow.has(item.threadId)) continue;\n if (!options.indexHasMore) continue;\n if (windowTailMs !== null && timestampMs(item.updatedAt) >= windowTailMs) continue;\n merged.push(item);\n }\n\n merged.sort((a, b) => timestampMs(b.updatedAt) - timestampMs(a.updatedAt));\n\n const unchanged =\n merged.length === page.items.length &&\n merged.every((item, index) => item === page.items[index]);\n if (unchanged) return page;\n\n return { ...page, items: merged };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"conversation-list.js","sources":["../../../../agent-runtime/src/conversation-list.ts"],"sourcesContent":["import type { InfiniteData, QueryClient } from '@tanstack/react-query';\nimport type { ConversationIndexEntry } from './conversation-index';\n\n/**\n * What a session asks of the viewer, derived server-side from the run, the\n * parked interrupt, the pull requests it opened, its termination and its last\n * wake (the Devin-style session list). `needs_you` is the only state that\n * demands the viewer act; `working` is live; `ready` finished with something\n * to look at; `watching` waits on an external party (a review, a check);\n * `done` and `error` are terminal. A backend newer than this client may report\n * a value outside the union — render it as \"no attention\" rather than failing.\n */\nexport type ConversationAttention =\n | 'needs_you'\n | 'working'\n | 'ready'\n | 'watching'\n | 'done'\n | 'error';\n\nexport const CONVERSATION_ATTENTIONS: readonly ConversationAttention[] = [\n 'needs_you',\n 'working',\n 'ready',\n 'watching',\n 'done',\n 'error',\n];\n\n/** Which signal produced the attention state (and the `nextAction` copy). */\nexport type ConversationAttentionSource =\n | 'interrupt'\n | 'run'\n | 'pull_request'\n | 'termination'\n | 'wake'\n | 'handoff'\n | 'follow_up';\n\n/** The last event that woke a resting session (a GitHub webhook, a schedule, a handoff). */\nexport interface ConversationWake {\n source: string | null;\n reason: string | null;\n /** ISO 8601 timestamp of the wake. */\n at: string | null;\n}\n\n/**\n * Live GitHub state of a pull request the session opened, read by the server\n * when the list renders. Never pushed by the conversation index — it comes\n * from the list fetch only, so a fold keeps the fetched value.\n */\nexport interface ConversationPullRequest {\n /** `owner/repo` the pull request lives in. */\n repoFullName: string;\n number: number;\n url: string;\n title: string;\n state: 'OPEN' | 'CLOSED' | 'MERGED';\n isDraft: boolean;\n /** GitHub's review decision (APPROVED, CHANGES_REQUESTED, REVIEW_REQUIRED) or null. */\n reviewDecision: string | null;\n /** Combined state of the checks on the head commit. */\n checksState: 'SUCCESS' | 'FAILURE' | 'PENDING' | 'NONE';\n /** ISO 8601 timestamp of the pull request's last update on GitHub. */\n updatedAt: string;\n}\n\n/**\n * Shared conversation/thread LIST contract (chat-stack unification —\n * thread-list-contract-convergence).\n *\n * Every chat surface lists \"my conversations\" against the same underlying\n * session assets, but historically each declared its own row shape, paging\n * model, and filter set (Olympus GraphQL `listConversations`, the SDK/Chrome\n * REST `/api/conversations/threads/list`, mobile's `/api/v0/sessions`). This\n * module is the ONE contract those surfaces converge on:\n *\n * - **Row shape**: {@link ConversationSummary} — the canonical camelCase\n * superset. Fields a transport cannot supply are `null`, never invented.\n * - **Ordering**: newest first by `updatedAt`, always. Ordering is part of the\n * contract, not a parameter — no surface sorts any other way.\n * - **Paging**: offset/limit with a LIVE newest page and explicitly-fetched\n * frozen history pages (see `useConversationList`). `hasMore` continues the\n * scan; `totalCount` is advisory and may be `null` for transports that do\n * not count.\n * - **Filtering**: {@link ConversationListFilter}. `appId` follows\n * `docs/SESSION_APP_ID_GUIDE.md`: exact match when set (legacy `null`\n * sessions excluded), no filter when unset. A fetcher that cannot express a\n * requested filter field MUST throw rather than silently ignore it — a\n * dropped filter returns wrong rows under a cache key that claims otherwise.\n * - **Freshness**: the per-user conversation-index statewire\n * (`conversation-index.ts`) is the push signal; these queries are the data\n * source. Push handlers call {@link invalidateConversationListCaches}.\n */\nexport interface ConversationSummary {\n /** LangGraph thread id — the cross-surface conversation identity. */\n threadId: string;\n title: string;\n /** ISO 8601 timestamp of last update (the contract's sort key). */\n updatedAt: string;\n /** ISO 8601 creation timestamp, when the transport reports it. */\n createdAt: string | null;\n /** Session lifecycle state from athena_metadata (e.g. running, completed). */\n state: string | null;\n /** Agent runtime marker (e.g. 'athena_deep_agent'), when reported. */\n runtime: string | null;\n /** Channel the session was started from (e.g. agent_email, web). */\n sourceChannel: string | null;\n /** Asset type (session, video_session, desktop_session, mobile_session). */\n assetType: string | null;\n /** Session asset id (`asset_<uuid>`), when the transport reports it. */\n assetId: string | null;\n /** Client app that created the session (SESSION_APP_ID_GUIDE). */\n appId: string | null;\n /** Computer asset whose sandbox started the session (`sourceChannel` 'computer'); omitted by surfaces that don't carry it. */\n computerAssetId?: string | null;\n /**\n * Collab agent (`asset_<uuid>`) and channel the session started on; `null`\n * channel is the agent's default channel. The server refuses a run on another\n * of the agent's channels, so a client shows the session's mode from these\n * instead of remembering it. Omitted by surfaces that don't carry them.\n */\n collabAgentId?: string | null;\n collabChannelId?: string | null;\n /** Session asset id this row is a sub-session of; omitted by surfaces that don't carry it. */\n parentSessionId?: string | null;\n /** Sub-sessions of this row the viewer can see; omitted by surfaces that don't carry it. */\n subSessionCount?: number;\n /** Trigger marker (`manual` remains user-initiated; other values are automated). */\n triggerType: string | null;\n /** Last-message preview used when a legacy session has no useful title. */\n preview: string | null;\n messageCount: number | null;\n /** Per-user unread flag derived server-side from the read receipt. */\n isUnread: boolean;\n /**\n * Canonical session status (status_v2: idle | active | needs_input | error),\n * or null when the transport does not report it.\n */\n sessionStatus: string | null;\n /**\n * Canonical status of the session's current run (status_v2: scheduled |\n * queued | running | needs_input | completed | failed | canceled). Null when\n * the transport does not report it — {@link isConversationRunning} then\n * falls back to the legacy `state`. Running badges on every surface derive\n * from this one helper so they cannot disagree.\n */\n runStatus: string | null;\n /**\n * Server-derived attention state. `undefined` when the transport (or an\n * older server) does not report it — {@link deriveConversationAttention}\n * then falls back to the status planes; `null` when the server reports none.\n */\n attention?: ConversationAttention | null;\n /** One line (≤ 80 chars) of what the viewer should do next, when the server derives one. */\n nextAction?: string | null;\n /** Which signal produced `attention` / `nextAction`. */\n attentionSource?: ConversationAttentionSource | null;\n /** Whether the viewer pinned (favorited) the session; `undefined` when not reported. */\n isFavorited?: boolean;\n /**\n * Whether the viewer started the session (`false`: it is shared with them);\n * `undefined` when not reported. Never pushed by the conversation index, so\n * a fold keeps the fetched value.\n */\n createdByMe?: boolean;\n /** The last wake of a resting session; `undefined` when not reported. */\n lastWake?: ConversationWake | null;\n /**\n * Pull requests the session opened with their live GitHub state. `[]` when\n * it opened none, `null` when the state could not be read right now,\n * `undefined` when the transport did not ask for (or report) it.\n */\n pullRequests?: ConversationPullRequest[] | null;\n}\n\n/**\n * The canonical filter set. Semantics are defined here once; each fetcher\n * translates the fields its transport supports and throws on the rest.\n */\nexport interface ConversationListFilter {\n /** Exact-match client app filter; unset = all apps (SESSION_APP_ID_GUIDE). */\n appId?: string;\n /**\n * Sessions of any of these client apps too, a union with `appId` (REST thread\n * list only today). A host lists an earlier app's sessions beside its own.\n */\n appIds?: readonly string[];\n /**\n * The collab agent (`asset_<uuid>`) whose home this list's app is (REST\n * thread list only today, sent as `home_agent_id`). When the agent has a home\n * app set and the viewer can open the agent, the server widens the app scope\n * (`appId` / `appIds`) to every conversation with the agent too; every other\n * filter still applies. The page says whether it did\n * ({@link ConversationListPage.homeAgentApplied}); an older server ignores\n * the field and lists the app scope alone.\n */\n homeAgentId?: string;\n /** Asset types to include (e.g. ['session']); unset = all session types. */\n assetTypes?: readonly string[];\n /** Case-insensitive title search. */\n search?: string;\n /** Hide triggered/background sessions (REST thread list only today). */\n excludeTriggered?: boolean;\n /** Start-channel filter (v0 sessions / REST thread list only today). */\n sourceChannels?: readonly string[];\n /** Start channels to hide (e.g. ['api']); GraphQL `listConversations` only today. */\n excludeSourceChannels?: readonly string[];\n /** Exact computer asset whose sandbox started the session; GraphQL `listConversations` only today. */\n computerAssetId?: string;\n /** Only the sub-sessions of this session asset id; GraphQL `listConversations` only today. */\n parentSessionId?: string;\n /**\n * Lifecycle-state filter (e.g. ['running'], ['completed']). Transports match\n * it with the shared status_v2 rule (running carries a 12-hour recency cutoff).\n */\n states?: readonly string[];\n /** Restrict to sessions run by any of these agents (athena_metadata.agent). */\n agentIds?: readonly string[];\n /**\n * Include branched sub-sessions and orchestration children, which every list\n * hides by default because they belong under their parent thread.\n */\n includeSubSessions?: boolean;\n}\n\nexport interface ConversationListPage {\n /** Rows in contract order (updatedAt desc). */\n items: ConversationSummary[];\n /** Whether another `offset + limit` scan would return more rows. */\n hasMore: boolean;\n /** Total matching rows, or null when the transport does not count. */\n totalCount: number | null;\n /**\n * Whether the server widened this page to the conversations with the\n * filter's `homeAgentId` agent (REST `home_agent_applied`). Absent (an older\n * server, another transport, or no `homeAgentId`) means it did not.\n */\n homeAgentApplied?: boolean;\n}\n\nexport interface ConversationListFetchParams {\n limit: number;\n offset: number;\n filter: ConversationListFilter;\n signal?: AbortSignal;\n}\n\n/**\n * One page fetch against some transport (GraphQL `listConversations`, the\n * REST thread list, …). Must reject on filter fields it cannot express.\n */\nexport type ConversationListFetcher = (\n params: ConversationListFetchParams\n) => Promise<ConversationListPage>;\n\n/** Root of every conversation-list query key (all surfaces, all filters). */\nexport const CONVERSATION_LIST_QUERY_KEY_ROOT = 'athena-conversation-list';\n\n/**\n * Scope used by `useConversationList` when the caller passes no `scopeKey`.\n * Exported so cache seeders (e.g. Olympus's spacesBootstrap adoption) can\n * address the exact keys default-scoped hooks read from.\n */\nexport const CONVERSATION_LIST_DEFAULT_SCOPE_KEY = 'default';\n\n/**\n * Normalize a filter to its semantic content: unset/empty fields are dropped\n * (so `{}` and `{ appId: undefined }` are the same filter) and array order is\n * canonicalized. Fetchers receive the normalized form, so what a cache entry\n * claims to hold is exactly what was fetched.\n */\nexport function normalizeConversationListFilter(\n filter: ConversationListFilter\n): ConversationListFilter {\n const normalized: ConversationListFilter = {};\n if (filter.appId) normalized.appId = filter.appId;\n if (filter.appIds && filter.appIds.length > 0) {\n normalized.appIds = [...new Set(filter.appIds)].sort();\n }\n if (filter.homeAgentId) normalized.homeAgentId = filter.homeAgentId;\n if (filter.assetTypes && filter.assetTypes.length > 0) {\n normalized.assetTypes = [...filter.assetTypes].sort();\n }\n if (filter.search) normalized.search = filter.search;\n if (filter.excludeTriggered !== undefined) normalized.excludeTriggered = filter.excludeTriggered;\n if (filter.sourceChannels && filter.sourceChannels.length > 0) {\n normalized.sourceChannels = [...filter.sourceChannels].sort();\n }\n if (filter.excludeSourceChannels && filter.excludeSourceChannels.length > 0) {\n normalized.excludeSourceChannels = [...filter.excludeSourceChannels].sort();\n }\n if (filter.computerAssetId) normalized.computerAssetId = filter.computerAssetId;\n if (filter.parentSessionId) normalized.parentSessionId = filter.parentSessionId;\n if (filter.states && filter.states.length > 0) {\n normalized.states = [...filter.states].sort();\n }\n if (filter.agentIds && filter.agentIds.length > 0) {\n normalized.agentIds = [...filter.agentIds].sort();\n }\n if (filter.includeSubSessions) normalized.includeSubSessions = true;\n return normalized;\n}\n\n/** status_v2 run statuses during which a session is actively working. */\nconst RUNNING_RUN_STATUSES: ReadonlySet<string> = new Set(['queued', 'running']);\n\n/**\n * THE running rule for every conversation-list surface (sidebar pane, recent\n * dropdown, workspace section, pushed index rows): the canonical status_v2 run\n * status when the transport reports one, else the legacy `state`. A session\n * paused on a human gate (`needs_input`) is not running.\n */\nexport function isConversationRunning(summary: {\n runStatus?: string | null;\n state?: string | null;\n}): boolean {\n if (summary.runStatus !== null && summary.runStatus !== undefined) {\n return RUNNING_RUN_STATUSES.has(summary.runStatus);\n }\n return summary.state === 'running';\n}\n\nconst KNOWN_ATTENTIONS: ReadonlySet<string> = new Set(CONVERSATION_ATTENTIONS);\n\n/**\n * THE attention rule for every session-list surface: the server-derived\n * `attention` when the transport reports one it knows, else a client fallback\n * from the status planes so an older server still yields the two states a\n * list cannot do without — a human gate reads as `needs_you`, a live run as\n * `working`, a failed run as `error`. `null` when nothing applies.\n */\nexport function deriveConversationAttention(\n summary: Pick<ConversationSummary, 'attention' | 'runStatus' | 'sessionStatus' | 'state'>\n): ConversationAttention | null {\n if (typeof summary.attention === 'string' && KNOWN_ATTENTIONS.has(summary.attention)) {\n return summary.attention;\n }\n if (summary.runStatus === 'needs_input' || summary.sessionStatus === 'needs_input') {\n return 'needs_you';\n }\n if (isConversationRunning(summary)) return 'working';\n if (summary.runStatus === 'failed' || summary.sessionStatus === 'error') return 'error';\n return null;\n}\n\n/**\n * Stable cache identity for a filter: the normalized fields serialized in\n * sorted key order, so semantically equal filters share one cache entry.\n */\nexport function conversationListFilterKey(filter: ConversationListFilter): string {\n const normalized = normalizeConversationListFilter(filter) as Record<string, unknown>;\n return JSON.stringify(\n Object.keys(normalized)\n .sort()\n .map((key) => [key, normalized[key]])\n );\n}\n\n/**\n * Query keys for the two-tier pager. `scopeKey` isolates caches beyond the\n * filter — surfaces MUST include everything that changes whose list this is\n * (principal/user id, backend base URL) so a credential swap can never read\n * another identity's rows out of the cache.\n *\n * `pageSize` is part of the key because it is baked into every cached page's\n * offset boundaries: two hooks sharing a scope and filter but paging by\n * different sizes would otherwise reuse each other's incompatible boundaries\n * and skip or duplicate rows.\n */\nexport const conversationListQueryKeys = {\n root: [CONVERSATION_LIST_QUERY_KEY_ROOT] as const,\n scope: (scopeKey: string) => [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey] as const,\n firstPage: (scopeKey: string, filterKey: string, pageSize: number) =>\n [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey, filterKey, pageSize, 'first-page'] as const,\n history: (scopeKey: string, filterKey: string, pageSize: number) =>\n [CONVERSATION_LIST_QUERY_KEY_ROOT, scopeKey, filterKey, pageSize, 'history'] as const,\n};\n\n/**\n * Flatten pages into one newest-first list, deduplicating by `threadId`.\n * The live first page wins over retained history copies of the same thread.\n */\nexport function mergeConversationPages(\n pages: readonly ConversationListPage[]\n): ConversationSummary[] {\n const seen = new Set<string>();\n const items: ConversationSummary[] = [];\n for (const page of pages) {\n for (const item of page.items) {\n if (seen.has(item.threadId)) continue;\n seen.add(item.threadId);\n items.push(item);\n }\n }\n return items;\n}\n\n/**\n * Prepend a conversation to a page unless its thread is already present\n * (optimistic \"new thread appears at the top\" write). Identity-preserving\n * no-op when present, so equal writes never notify subscribers.\n */\nexport function prependConversationToPage(\n page: ConversationListPage,\n item: ConversationSummary\n): ConversationListPage {\n if (page.items.some((existing) => existing.threadId === item.threadId)) return page;\n return {\n ...page,\n items: [item, ...page.items],\n totalCount: page.totalCount === null ? null : page.totalCount + 1,\n };\n}\n\n/** Remove a thread's row from a page; identity-preserving when absent. */\nexport function removeConversationFromPage(\n page: ConversationListPage,\n threadId: string\n): ConversationListPage {\n if (!page.items.some((item) => item.threadId === threadId)) return page;\n return { ...page, items: page.items.filter((item) => item.threadId !== threadId) };\n}\n\n/**\n * Patch every row matching `match`; identity-preserving when nothing matches\n * so cache writes cannot cause spurious re-renders.\n */\nexport function patchConversationsInPage(\n page: ConversationListPage,\n match: (item: ConversationSummary) => boolean,\n patch: Partial<ConversationSummary>\n): ConversationListPage {\n let changed = false;\n const items = page.items.map((item) => {\n if (!match(item)) return item;\n changed = true;\n return { ...item, ...patch };\n });\n return changed ? { ...page, items } : page;\n}\n\ntype CachedConversationList =\n | ConversationListPage\n | InfiniteData<ConversationListPage, number>\n | undefined;\n\nfunction isInfinitePages(\n data: ConversationListPage | InfiniteData<ConversationListPage, number>\n): data is InfiniteData<ConversationListPage, number> {\n return 'pages' in data;\n}\n\nfunction mapCachedPages(\n data: CachedConversationList,\n transform: (page: ConversationListPage) => ConversationListPage\n): CachedConversationList {\n if (!data) return data;\n if (isInfinitePages(data)) {\n let changed = false;\n const pages = data.pages.map((page) => {\n const next = transform(page);\n if (next !== page) changed = true;\n return next;\n });\n return changed ? { ...data, pages } : data;\n }\n return transform(data);\n}\n\ninterface ConversationListCacheScope {\n /** Restrict to one scope; omitted = every conversation-list cache. */\n scopeKey?: string;\n}\n\nfunction scopedKey(options?: ConversationListCacheScope): readonly string[] {\n return options?.scopeKey !== undefined\n ? conversationListQueryKeys.scope(options.scopeKey)\n : conversationListQueryKeys.root;\n}\n\n/**\n * Invalidate conversation-list caches (all filters, first page + history).\n * This is the hook for push signals — the conversation-index statewire, a\n * finished run, an archive/restore — anything that changed the server list.\n */\nexport function invalidateConversationListCaches(\n queryClient: QueryClient,\n options?: ConversationListCacheScope\n): Promise<void> {\n return queryClient.invalidateQueries({ queryKey: scopedKey(options) });\n}\n\n/** Cancel in-flight list fetches so an optimistic write cannot be overwritten. */\nexport function cancelConversationListCaches(\n queryClient: QueryClient,\n options?: ConversationListCacheScope\n): Promise<void> {\n return queryClient.cancelQueries({ queryKey: scopedKey(options) });\n}\n\n/**\n * Patch matching rows in every cached conversation list (first pages, history\n * pages, searches) so per-row state (read receipts, titles) flips in the same\n * frame as the interaction instead of after the next refetch.\n */\nexport function patchConversationListCaches(\n queryClient: QueryClient,\n options: ConversationListCacheScope & {\n match: (item: ConversationSummary) => boolean;\n patch: Partial<ConversationSummary>;\n }\n): void {\n queryClient.setQueriesData<CachedConversationList>({ queryKey: scopedKey(options) }, (data) =>\n mapCachedPages(data, (page) => patchConversationsInPage(page, options.match, options.patch))\n );\n}\n\n/** Remove a thread's row from every cached conversation list in scope. */\nexport function removeConversationFromListCaches(\n queryClient: QueryClient,\n options: ConversationListCacheScope & { threadId: string }\n): void {\n queryClient.setQueriesData<CachedConversationList>({ queryKey: scopedKey(options) }, (data) =>\n mapCachedPages(data, (page) => removeConversationFromPage(page, options.threadId))\n );\n}\n\nfunction timestampMs(value: string): number {\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? 0 : parsed;\n}\n\nfunction sameSummary(a: ConversationSummary, b: ConversationSummary): boolean {\n return (\n a.threadId === b.threadId &&\n a.title === b.title &&\n a.updatedAt === b.updatedAt &&\n a.createdAt === b.createdAt &&\n a.state === b.state &&\n a.runtime === b.runtime &&\n a.sourceChannel === b.sourceChannel &&\n a.assetType === b.assetType &&\n a.assetId === b.assetId &&\n a.appId === b.appId &&\n a.triggerType === b.triggerType &&\n a.preview === b.preview &&\n a.messageCount === b.messageCount &&\n a.isUnread === b.isUnread &&\n a.sessionStatus === b.sessionStatus &&\n a.runStatus === b.runStatus &&\n a.attention === b.attention &&\n a.nextAction === b.nextAction &&\n a.attentionSource === b.attentionSource &&\n a.isFavorited === b.isFavorited &&\n // A transport that does not carry the agent (`undefined`) and a push that\n // says the session has none (`null`) agree.\n (a.collabAgentId ?? null) === (b.collabAgentId ?? null) &&\n // Never pushed by the index, so a fold carries the previous references\n // through unchanged; identity is the equality that matters here.\n a.lastWake === b.lastWake &&\n a.pullRequests === b.pullRequests\n );\n}\n\n/**\n * Project one replicated conversation-index row onto the shared summary shape.\n * Fields the index never carries (`createdAt`) and viewer-specific fields a\n * lifecycle publish cannot know (`is_unread: null`) keep the row's previous\n * value. Canonical status is taken from the entry when the publishing pod\n * reports it; a snapshot from a pod that predates the fields falls back to the\n * legacy `state` (null status) rather than keeping a stale run status.\n *\n * Attention fields (`attention`, `next_action`, `attention_source`,\n * `is_favorited`) are taken from the entry when the publishing pod reports\n * them, so a live push flips a row to \"needs you\" the way it renames it; a\n * pod that predates them leaves the row's previous values. The collab agent\n * (`collab_agent_id`) is taken the same way. Pull-request state, the last wake\n * and `createdByMe` are never pushed and always keep the fetched value.\n */\nexport function summaryFromConversationIndexEntry(\n entry: ConversationIndexEntry,\n previous?: ConversationSummary\n): ConversationSummary {\n const carriesStatus = entry.run_status !== undefined || entry.session_status !== undefined;\n return {\n threadId: entry.thread_id,\n title: entry.title,\n updatedAt: entry.updated_at,\n createdAt: previous?.createdAt ?? null,\n state: entry.state,\n runtime: entry.runtime,\n sourceChannel: entry.start_channel,\n assetType: entry.asset_type,\n assetId: entry.asset_id ?? previous?.assetId ?? null,\n computerAssetId: previous?.computerAssetId ?? null,\n collabAgentId:\n entry.collab_agent_id !== undefined ? entry.collab_agent_id : previous?.collabAgentId,\n collabChannelId: previous?.collabChannelId,\n parentSessionId: previous?.parentSessionId,\n subSessionCount: previous?.subSessionCount,\n appId: entry.app_id,\n triggerType: entry.trigger_type,\n preview: entry.last_message_preview,\n messageCount: entry.message_count,\n isUnread: entry.is_unread ?? previous?.isUnread ?? false,\n sessionStatus: carriesStatus ? (entry.session_status ?? null) : null,\n runStatus: carriesStatus ? (entry.run_status ?? null) : null,\n attention: entry.attention !== undefined ? entry.attention : previous?.attention,\n nextAction: entry.next_action !== undefined ? entry.next_action : previous?.nextAction,\n attentionSource:\n entry.attention_source !== undefined ? entry.attention_source : previous?.attentionSource,\n isFavorited: entry.is_favorited ?? previous?.isFavorited,\n createdByMe: previous?.createdByMe,\n lastWake: previous?.lastWake,\n pullRequests: previous?.pullRequests,\n };\n}\n\n/**\n * Fold the user's replicated conversation index (the newest-page window the\n * per-user statewire pushes) into a cached live first page, so a thread\n * created, renamed, finished, or read anywhere updates every list surface in\n * the same frame — without refetching the page.\n *\n * - Rows in the window are upserted (new threads appear, existing rows take\n * the pushed fields, see {@link summaryFromConversationIndexEntry}).\n * - Rows NOT in the window but at least as new as the window's oldest row\n * should have been in it, so they left the list (archived, hidden, shared\n * away) and are dropped. Older rows are beyond the window and are kept.\n * - When the index reports no further rows (`indexHasMore === false`) the\n * window IS the whole list, so every row outside it is dropped.\n *\n * Newest-first ordering is restored and the page identity is preserved when\n * nothing changed, so an equal push never notifies subscribers.\n */\nexport function mergeConversationIndexIntoPage(\n page: ConversationListPage,\n entries: readonly ConversationIndexEntry[],\n options: { indexHasMore: boolean }\n): ConversationListPage {\n const previousByThread = new Map(page.items.map((item) => [item.threadId, item]));\n const inWindow = new Set(entries.map((entry) => entry.thread_id));\n const windowTailMs =\n entries.length > 0 ? Math.min(...entries.map((entry) => timestampMs(entry.updated_at))) : null;\n\n const merged: ConversationSummary[] = entries.map((entry) => {\n const previous = previousByThread.get(entry.thread_id);\n const next = summaryFromConversationIndexEntry(entry, previous);\n return previous && sameSummary(previous, next) ? previous : next;\n });\n\n for (const item of page.items) {\n if (inWindow.has(item.threadId)) continue;\n if (!options.indexHasMore) continue;\n if (windowTailMs !== null && timestampMs(item.updatedAt) >= windowTailMs) continue;\n merged.push(item);\n }\n\n merged.sort((a, b) => timestampMs(b.updatedAt) - timestampMs(a.updatedAt));\n\n const unchanged =\n merged.length === page.items.length &&\n merged.every((item, index) => item === page.items[index]);\n if (unchanged) return page;\n\n return { ...page, items: merged };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -70,6 +70,8 @@ export declare const desktopHandoffViewSchema: z.ZodObject<{
|
|
|
70
70
|
voice: "voice";
|
|
71
71
|
}>>;
|
|
72
72
|
draft: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
73
|
+
cursor: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
74
|
+
revision: z.ZodOptional<z.ZodNumber>;
|
|
73
75
|
}, z.core.$strip>;
|
|
74
76
|
export type DesktopHandoffView = z.infer<typeof desktopHandoffViewSchema>;
|
|
75
77
|
export declare const desktopHandoffLaunchSchema: z.ZodObject<{
|
|
@@ -129,6 +131,8 @@ export declare const createDesktopHandoffResponseSchema: z.ZodObject<{
|
|
|
129
131
|
voice: "voice";
|
|
130
132
|
}>>;
|
|
131
133
|
draft: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
134
|
+
cursor: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
135
|
+
revision: z.ZodOptional<z.ZodNumber>;
|
|
132
136
|
}, z.core.$strip>;
|
|
133
137
|
launch: z.ZodObject<{
|
|
134
138
|
kind: z.ZodLiteral<"desktop">;
|
|
@@ -144,6 +148,12 @@ export interface CreateDesktopHandoffInput {
|
|
|
144
148
|
assets: DesktopHandoffAsset[];
|
|
145
149
|
active_asset_id?: string | null;
|
|
146
150
|
source: "spaces";
|
|
151
|
+
/**
|
|
152
|
+
* Where the source was reading (a message id, or `""` at the bottom). Only to
|
|
153
|
+
* a host whose readiness says `supports_handoff_cursor`: the route refuses
|
|
154
|
+
* unknown fields.
|
|
155
|
+
*/
|
|
156
|
+
cursor?: string;
|
|
147
157
|
}
|
|
148
158
|
/** Only a destination acknowledgement proves a launch. Timeouts prove no installation state. */
|
|
149
159
|
export declare function desktopHandoffSummary({ handoff, }: {
|
|
@@ -59,6 +59,25 @@ function buildDeepAgentEditCommand({
|
|
|
59
59
|
runAnchorMessageId
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
function deepAgentHistoryHasOlder(extras) {
|
|
63
|
+
var _a;
|
|
64
|
+
const typed = readDeepAgentStatewireExtras(extras);
|
|
65
|
+
if (typed === null) return false;
|
|
66
|
+
return ((_a = readWindow(typed)) == null ? void 0 : _a.hasOlder) ?? null;
|
|
67
|
+
}
|
|
68
|
+
async function loadOlderDeepAgentHistoryPage({
|
|
69
|
+
readExtras,
|
|
70
|
+
pageSize
|
|
71
|
+
}) {
|
|
72
|
+
const extras = readExtras();
|
|
73
|
+
if (!(extras == null ? void 0 : extras.invoke)) return false;
|
|
74
|
+
const window = readWindow(extras);
|
|
75
|
+
if (window === null || !window.hasOlder) return false;
|
|
76
|
+
const requests = messageWindow.pageInterestRequests(window, pageSize);
|
|
77
|
+
if (requests === null) return false;
|
|
78
|
+
await extras.invoke(runconfigContract.DEEP_AGENT_INTEREST_PROTOCOL, "set", [requests]);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
62
81
|
const MAX_EDIT_ANCHOR_HISTORY_LOADS = 5;
|
|
63
82
|
async function resolveDeepAgentEditCommand({
|
|
64
83
|
readExtras,
|
|
@@ -86,7 +105,9 @@ async function resolveDeepAgentEditCommand({
|
|
|
86
105
|
}
|
|
87
106
|
exports.MAX_EDIT_ANCHOR_HISTORY_LOADS = MAX_EDIT_ANCHOR_HISTORY_LOADS;
|
|
88
107
|
exports.buildDeepAgentEditCommand = buildDeepAgentEditCommand;
|
|
108
|
+
exports.deepAgentHistoryHasOlder = deepAgentHistoryHasOlder;
|
|
89
109
|
exports.isSourceBelowLoadedWindow = isSourceBelowLoadedWindow;
|
|
110
|
+
exports.loadOlderDeepAgentHistoryPage = loadOlderDeepAgentHistoryPage;
|
|
90
111
|
exports.readDeepAgentStatewireExtras = readDeepAgentStatewireExtras;
|
|
91
112
|
exports.resolveDeepAgentEditCommand = resolveDeepAgentEditCommand;
|
|
92
113
|
exports.resolveWireAnchorMessageId = resolveWireAnchorMessageId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-command.cjs","sources":["../../../../agent-runtime/src/edit-command.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n type DeepAgentMessageWindow,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\n\n/**\n * Wire-accurate `run/edit` construction for deep-agent retry/edit, shared by\n * every surface that resubmits a user message.\n *\n * assistant-ui builds the command's `runAnchorMessageId` from the CONVERTED\n * thread messages (`resolveEditCommand`: `messages[index - 1].id`), but the\n * deep-agent host validates that anchor against the edited message's parent in\n * its RAW message list (`harness_sdk.linear_thread`: the entry before it in\n * `state[\"messages\"]`). Those two lists do not line up:\n *\n * - the converter chunks an assistant turn and its tool messages into ONE\n * thread message carrying only the FIRST wire id, so the anchor names the\n * head of the previous turn instead of its last tool message;\n * - `sanitizeLangChainMessages` drops hidden-from-frontend and unconvertible\n * entries, so a dropped tail message is invisible to the anchor too.\n *\n * Either way the host rejects the command with `wrong-anchor`\n * (`runAnchorMessageId … is not the source's parent`) AFTER the composer's\n * synchronous `send()` returned, so the retry/edit silently does nothing.\n *\n * Building the command here off the replicated raw state makes the anchor the\n * same value the host computes.\n *\n * The raw list is the loaded interest window, so the first loaded message's\n * parent is not in it while older messages exist — an edit there must not\n * anchor on `null` (the root, rejected `wrong-anchor`); a message below the\n * window has no parent on this client at all. {@link resolveDeepAgentEditCommand}\n * states older page interests until the parent is loaded.\n */\n\n/**\n * Host metadata frozen at submission, settling once the async capture does.\n * The message-metadata preparation surface — `prepareMessageMetadata`,\n * `discardPreparedMessageMetadata`, `sendPreparedEdit` and `sendCommand`'s\n * `preparedMetadata` option — is what the monorepo patches into\n * `@assistant-ui/react-statewire@0.31.0` (`patches/`). It is declared\n * structurally here rather than as `StatewireThread.Extras[...]` so that the\n * published sources also compile against the unpatched upstream typings, where\n * those members are absent and the edit path's optional calls never run.\n */\nexport type DeepAgentPreparedMessageMetadata = Promise<Record<string, unknown> | undefined>;\n\n/** The pieces of the statewire thread extras the edit path reads. */\nexport interface DeepAgentStatewireExtras {\n /** The library's dispatch; it resolves with the host's reply to the command. */\n sendCommand: (\n command: StatewireThread.Command,\n options?: { preparedMetadata?: DeepAgentPreparedMessageMetadata | undefined },\n ) => Promise<unknown> | void;\n /** Optional on older hosts; capture before loading an edit's history anchor. */\n prepareMessageMetadata?: (() => DeepAgentPreparedMessageMetadata) | undefined;\n discardPreparedMessageMetadata?:\n | ((prepared: DeepAgentPreparedMessageMetadata) => void)\n | undefined;\n sendPreparedEdit?:\n | ((sourceId: string, prepared: DeepAgentPreparedMessageMetadata) => void)\n | undefined;\n isPreparingSubmission?: boolean;\n /** The composed thread state — its `messages` are the raw wire messages of the loaded window. */\n state: { messages?: readonly { id?: unknown }[] } | null;\n /** The mounted documents (the interest window), absent on a wire without documents. */\n documents?: StatewireThread.Extras['documents'];\n invoke?: StatewireThread.Extras['invoke'];\n /** The live run, when one is in flight; `epoch` advances on each dispatch. */\n runs: { runId?: string | null; epoch?: number };\n /** The host's replicated run status; absent on a surface that predates it. */\n status?: StatewireThread.ThreadStatus;\n}\n\n/**\n * The statewire extras of the nearest aui thread, or `null` when the thread is\n * not a statewire thread (e.g. a read-only subagent transcript runtime).\n * Gated on {@link hasStatewireThreadExtras} for the same reason the approval\n * card is: the library's own hooks throw on a non-statewire thread.\n */\nexport function readDeepAgentStatewireExtras(extras: unknown): DeepAgentStatewireExtras | null {\n if (!hasStatewireThreadExtras(extras)) return null;\n return extras as DeepAgentStatewireExtras;\n}\n\n/**\n * The host's parent of `sourceId`: the raw wire message directly before it\n * (`null` when it is the first of the whole transcript). Returns `undefined`\n * when `sourceId` is not in the raw list at all — an optimistic bubble the\n * host has not settled yet, a message below the loaded window, or a surface\n * whose state this module cannot read — or when it is the first loaded\n * message while older ones exist (`hasOlder`), so the caller can load more or\n * fall back to the library's own path instead of asserting a wrong anchor.\n */\nexport function resolveWireAnchorMessageId({\n wireMessages,\n sourceId,\n hasOlder = false,\n}: {\n wireMessages: readonly { id?: unknown }[] | undefined;\n sourceId: string;\n hasOlder?: boolean;\n}): string | null | undefined {\n if (!wireMessages) return undefined;\n const index = wireMessages.findIndex((message) => message?.id === sourceId);\n if (index < 0) return undefined;\n if (index === 0) return hasOlder ? undefined : null;\n const parentId = wireMessages[index - 1]?.id;\n return typeof parentId === 'string' ? parentId : undefined;\n}\n\n/**\n * Whether `sourceId`'s parent lies below the loaded window: the message is\n * absent from the raw list, or its first entry, while older messages exist.\n * The caller loads older history before an edit can be anchored.\n */\nexport function isSourceBelowLoadedWindow({\n wireMessages,\n sourceId,\n window,\n}: {\n wireMessages: readonly { id?: unknown }[] | undefined;\n sourceId: string;\n window: DeepAgentMessageWindow | null;\n}): boolean {\n if (!wireMessages || window === null || !window.hasOlder) return false;\n const index = wireMessages.findIndex((message) => message?.id === sourceId);\n return index <= 0;\n}\n\nconst readWindow = (extras: DeepAgentStatewireExtras) =>\n readDeepAgentMessageWindow(extras.documents?.list());\n\n/**\n * A fresh command / message id (uniqueness, not secrecy). `crypto.randomUUID`\n * where the host runtime has it — every browser, Bun, Node and Hermes with\n * webcrypto — and a v4-shaped fallback elsewhere, so the shared path never\n * throws on a runtime without full webcrypto.\n */\nfunction mintCommandId(): string {\n const webcrypto = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (typeof webcrypto?.randomUUID === 'function') return webcrypto.randomUUID();\n const hex = (): string => Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0');\n return `${hex()}${hex()}-${hex()}-4${hex().slice(1)}-${hex()}-${hex()}${hex()}${hex()}`;\n}\n\n/**\n * The `run/edit` command for resubmitting `sourceId` with `text`, anchored on\n * the host's own parent. `null` when the anchor cannot be resolved from the\n * replicated state.\n */\nexport function buildDeepAgentEditCommand({\n extras,\n sourceId,\n text,\n}: {\n extras: DeepAgentStatewireExtras;\n sourceId: string;\n text: string;\n}): StatewireThread.RunEditCommand | null {\n const runAnchorMessageId = resolveWireAnchorMessageId({\n wireMessages: extras.state?.messages,\n sourceId,\n hasOlder: readWindow(extras)?.hasOlder ?? false,\n });\n if (runAnchorMessageId === undefined) return null;\n return {\n type: 'run/edit',\n runId: extras.runs.runId ?? mintCommandId(),\n message: { id: mintCommandId(), role: 'user', parts: [{ type: 'text', text }] },\n sourceId,\n runAnchorMessageId,\n };\n}\n\n/**\n * State one more page of older history for a windowed thread: the next page\n * past the loaded window's oldest message. Resolves `true` once the host has\n * mounted it (it answers the statement after mounting), `false` when there is\n * nothing older, the thread is not a windowed statewire thread, or no page can\n * be stated. For a reader that needs a message below the loaded window — a\n * search result revealing an older turn of a long conversation — and pages\n * until it renders.\n */\nexport async function loadOlderDeepAgentHistoryPage({\n readExtras,\n pageSize,\n}: {\n readExtras: () => DeepAgentStatewireExtras | null;\n pageSize?: number;\n}): Promise<boolean> {\n const extras = readExtras();\n if (!extras?.invoke) return false;\n const window = readWindow(extras);\n if (window === null || !window.hasOlder) return false;\n const requests = pageInterestRequests(window, pageSize);\n if (requests === null) return false;\n await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [requests]);\n return true;\n}\n\n/**\n * Pages of older history stated while looking for an edited message's host\n * parent. Each page is up to a window's worth of messages, so an edit reaches\n * this many windows below what was loaded before it gives up.\n */\nexport const MAX_EDIT_ANCHOR_HISTORY_LOADS = 5;\n\n/**\n * The anchored `run/edit` for `sourceId`, stating older page interests one at\n * a time when the message's parent lies below the loaded window. Every page\n * is read back from the live documents (`readExtras` — the host mounts them\n * before it answers the statement), so the anchor is always the host's own\n * parent id.\n *\n * Resolves `null` — the caller's cue for the library's composer path, whose\n * failure it reports — when the thread is not a statewire thread, the host has\n * not settled the message, the history is exhausted without it, or the page\n * budget ({@link MAX_EDIT_ANCHOR_HISTORY_LOADS}) is spent. A synchronous\n * resolution never awaits: the first read either anchors at once or decides\n * that pages are needed.\n */\nexport async function resolveDeepAgentEditCommand({\n readExtras,\n sourceId,\n text,\n pageSize,\n maxHistoryLoads = MAX_EDIT_ANCHOR_HISTORY_LOADS,\n}: {\n /** The live thread extras — re-read per page so each anchor attempt sees the widened window. */\n readExtras: () => DeepAgentStatewireExtras | null;\n sourceId: string;\n text: string;\n pageSize?: number;\n maxHistoryLoads?: number;\n}): Promise<StatewireThread.RunEditCommand | null> {\n for (let page = 0; page <= maxHistoryLoads; page += 1) {\n const extras = readExtras();\n if (!extras) return null;\n const command = buildDeepAgentEditCommand({ extras, sourceId, text });\n if (command) return command;\n if (page === maxHistoryLoads || !extras.invoke) return null;\n const window = readWindow(extras);\n if (!isSourceBelowLoadedWindow({ wireMessages: extras.state?.messages, sourceId, window })) {\n return null;\n }\n const requests = window === null ? null : pageInterestRequests(window, pageSize);\n if (requests === null) return null;\n await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [requests]);\n }\n return null;\n}\n"],"names":["hasStatewireThreadExtras","readDeepAgentMessageWindow","pageInterestRequests","DEEP_AGENT_INTEREST_PROTOCOL"],"mappings":";;;;;AAoFO,SAAS,6BAA6B,QAAkD;AAC7F,MAAI,CAACA,aAAAA,yBAAyB,MAAM,EAAG,QAAO;AAC9C,SAAO;AACT;AAWO,SAAS,2BAA2B;AAAA,EACzC;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAI8B;;AAC5B,MAAI,CAAC,aAAc,QAAO;AAC1B,QAAM,QAAQ,aAAa,UAAU,CAAC,aAAY,mCAAS,QAAO,QAAQ;AAC1E,MAAI,QAAQ,EAAG,QAAO;AACtB,MAAI,UAAU,EAAG,QAAO,WAAW,SAAY;AAC/C,QAAM,YAAW,kBAAa,QAAQ,CAAC,MAAtB,mBAAyB;AAC1C,SAAO,OAAO,aAAa,WAAW,WAAW;AACnD;AAOO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AACF,GAIY;AACV,MAAI,CAAC,gBAAgB,WAAW,QAAQ,CAAC,OAAO,SAAU,QAAO;AACjE,QAAM,QAAQ,aAAa,UAAU,CAAC,aAAY,mCAAS,QAAO,QAAQ;AAC1E,SAAO,SAAS;AAClB;AAEA,MAAM,aAAa,CAAC,WAAA;;AAClBC,uBAAAA,4BAA2B,YAAO,cAAP,mBAAkB,MAAM;AAAA;AAQrD,SAAS,gBAAwB;AAC/B,QAAM,YAAa,WAA0D;AAC7E,MAAI,QAAO,uCAAW,gBAAe,WAAY,QAAO,UAAU,WAAA;AAClE,QAAM,MAAM,MAAc,KAAK,MAAM,KAAK,OAAA,IAAW,KAAO,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAC1F,SAAO,GAAG,IAAA,CAAK,GAAG,IAAA,CAAK,IAAI,IAAA,CAAK,KAAK,MAAM,MAAM,CAAC,CAAC,IAAI,IAAA,CAAK,IAAI,KAAK,GAAG,IAAA,CAAK,GAAG,IAAA,CAAK;AACvF;AAOO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AACF,GAI0C;;AACxC,QAAM,qBAAqB,2BAA2B;AAAA,IACpD,eAAc,YAAO,UAAP,mBAAc;AAAA,IAC5B;AAAA,IACA,YAAU,gBAAW,MAAM,MAAjB,mBAAoB,aAAY;AAAA,EAAA,CAC3C;AACD,MAAI,uBAAuB,OAAW,QAAO;AAC7C,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO,OAAO,KAAK,SAAS,cAAA;AAAA,IAC5B,SAAS,EAAE,IAAI,cAAA,GAAiB,MAAM,QAAQ,OAAO,CAAC,EAAE,MAAM,QAAQ,KAAA,CAAM,EAAA;AAAA,IAC5E;AAAA,IACA;AAAA,EAAA;AAEJ;AAiCO,MAAM,gCAAgC;AAgB7C,eAAsB,4BAA4B;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AACpB,GAOmD;;AACjD,WAAS,OAAO,GAAG,QAAQ,iBAAiB,QAAQ,GAAG;AACrD,UAAM,SAAS,WAAA;AACf,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,UAAU,0BAA0B,EAAE,QAAQ,UAAU,MAAM;AACpE,QAAI,QAAS,QAAO;AACpB,QAAI,SAAS,mBAAmB,CAAC,OAAO,OAAQ,QAAO;AACvD,UAAM,SAAS,WAAW,MAAM;AAChC,QAAI,CAAC,0BAA0B,EAAE,eAAc,YAAO,UAAP,mBAAc,UAAU,UAAU,OAAA,CAAQ,GAAG;AAC1F,aAAO;AAAA,IACT;AACA,UAAM,WAAW,WAAW,OAAO,OAAOC,cAAAA,qBAAqB,QAAQ,QAAQ;AAC/E,QAAI,aAAa,KAAM,QAAO;AAC9B,UAAM,OAAO,OAAOC,kBAAAA,8BAA8B,OAAO,CAAC,QAAQ,CAAC;AAAA,EACrE;AACA,SAAO;AACT;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"edit-command.cjs","sources":["../../../../agent-runtime/src/edit-command.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n type DeepAgentMessageWindow,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\n\n/**\n * Wire-accurate `run/edit` construction for deep-agent retry/edit, shared by\n * every surface that resubmits a user message.\n *\n * assistant-ui builds the command's `runAnchorMessageId` from the CONVERTED\n * thread messages (`resolveEditCommand`: `messages[index - 1].id`), but the\n * deep-agent host validates that anchor against the edited message's parent in\n * its RAW message list (`harness_sdk.linear_thread`: the entry before it in\n * `state[\"messages\"]`). Those two lists do not line up:\n *\n * - the converter chunks an assistant turn and its tool messages into ONE\n * thread message carrying only the FIRST wire id, so the anchor names the\n * head of the previous turn instead of its last tool message;\n * - `sanitizeLangChainMessages` drops hidden-from-frontend and unconvertible\n * entries, so a dropped tail message is invisible to the anchor too.\n *\n * Either way the host rejects the command with `wrong-anchor`\n * (`runAnchorMessageId … is not the source's parent`) AFTER the composer's\n * synchronous `send()` returned, so the retry/edit silently does nothing.\n *\n * Building the command here off the replicated raw state makes the anchor the\n * same value the host computes.\n *\n * The raw list is the loaded interest window, so the first loaded message's\n * parent is not in it while older messages exist — an edit there must not\n * anchor on `null` (the root, rejected `wrong-anchor`); a message below the\n * window has no parent on this client at all. {@link resolveDeepAgentEditCommand}\n * states older page interests until the parent is loaded.\n */\n\n/**\n * Host metadata frozen at submission, settling once the async capture does.\n * The message-metadata preparation surface — `prepareMessageMetadata`,\n * `discardPreparedMessageMetadata`, `sendPreparedEdit` and `sendCommand`'s\n * `preparedMetadata` option — is what the monorepo patches into\n * `@assistant-ui/react-statewire@0.31.0` (`patches/`). It is declared\n * structurally here rather than as `StatewireThread.Extras[...]` so that the\n * published sources also compile against the unpatched upstream typings, where\n * those members are absent and the edit path's optional calls never run.\n */\nexport type DeepAgentPreparedMessageMetadata = Promise<Record<string, unknown> | undefined>;\n\n/** The pieces of the statewire thread extras the edit path reads. */\nexport interface DeepAgentStatewireExtras {\n /** The library's dispatch; it resolves with the host's reply to the command. */\n sendCommand: (\n command: StatewireThread.Command,\n options?: { preparedMetadata?: DeepAgentPreparedMessageMetadata | undefined },\n ) => Promise<unknown> | void;\n /** Optional on older hosts; capture before loading an edit's history anchor. */\n prepareMessageMetadata?: (() => DeepAgentPreparedMessageMetadata) | undefined;\n discardPreparedMessageMetadata?:\n | ((prepared: DeepAgentPreparedMessageMetadata) => void)\n | undefined;\n sendPreparedEdit?:\n | ((sourceId: string, prepared: DeepAgentPreparedMessageMetadata) => void)\n | undefined;\n isPreparingSubmission?: boolean;\n /** The composed thread state — its `messages` are the raw wire messages of the loaded window. */\n state: { messages?: readonly { id?: unknown }[] } | null;\n /** The mounted documents (the interest window), absent on a wire without documents. */\n documents?: StatewireThread.Extras['documents'];\n invoke?: StatewireThread.Extras['invoke'];\n /** The live run, when one is in flight; `epoch` advances on each dispatch. */\n runs: { runId?: string | null; epoch?: number };\n /** The host's replicated run status; absent on a surface that predates it. */\n status?: StatewireThread.ThreadStatus;\n}\n\n/**\n * The statewire extras of the nearest aui thread, or `null` when the thread is\n * not a statewire thread (e.g. a read-only subagent transcript runtime).\n * Gated on {@link hasStatewireThreadExtras} for the same reason the approval\n * card is: the library's own hooks throw on a non-statewire thread.\n */\nexport function readDeepAgentStatewireExtras(extras: unknown): DeepAgentStatewireExtras | null {\n if (!hasStatewireThreadExtras(extras)) return null;\n return extras as DeepAgentStatewireExtras;\n}\n\n/**\n * The host's parent of `sourceId`: the raw wire message directly before it\n * (`null` when it is the first of the whole transcript). Returns `undefined`\n * when `sourceId` is not in the raw list at all — an optimistic bubble the\n * host has not settled yet, a message below the loaded window, or a surface\n * whose state this module cannot read — or when it is the first loaded\n * message while older ones exist (`hasOlder`), so the caller can load more or\n * fall back to the library's own path instead of asserting a wrong anchor.\n */\nexport function resolveWireAnchorMessageId({\n wireMessages,\n sourceId,\n hasOlder = false,\n}: {\n wireMessages: readonly { id?: unknown }[] | undefined;\n sourceId: string;\n hasOlder?: boolean;\n}): string | null | undefined {\n if (!wireMessages) return undefined;\n const index = wireMessages.findIndex((message) => message?.id === sourceId);\n if (index < 0) return undefined;\n if (index === 0) return hasOlder ? undefined : null;\n const parentId = wireMessages[index - 1]?.id;\n return typeof parentId === 'string' ? parentId : undefined;\n}\n\n/**\n * Whether `sourceId`'s parent lies below the loaded window: the message is\n * absent from the raw list, or its first entry, while older messages exist.\n * The caller loads older history before an edit can be anchored.\n */\nexport function isSourceBelowLoadedWindow({\n wireMessages,\n sourceId,\n window,\n}: {\n wireMessages: readonly { id?: unknown }[] | undefined;\n sourceId: string;\n window: DeepAgentMessageWindow | null;\n}): boolean {\n if (!wireMessages || window === null || !window.hasOlder) return false;\n const index = wireMessages.findIndex((message) => message?.id === sourceId);\n return index <= 0;\n}\n\nconst readWindow = (extras: DeepAgentStatewireExtras) =>\n readDeepAgentMessageWindow(extras.documents?.list());\n\n/**\n * A fresh command / message id (uniqueness, not secrecy). `crypto.randomUUID`\n * where the host runtime has it — every browser, Bun, Node and Hermes with\n * webcrypto — and a v4-shaped fallback elsewhere, so the shared path never\n * throws on a runtime without full webcrypto.\n */\nfunction mintCommandId(): string {\n const webcrypto = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (typeof webcrypto?.randomUUID === 'function') return webcrypto.randomUUID();\n const hex = (): string => Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0');\n return `${hex()}${hex()}-${hex()}-4${hex().slice(1)}-${hex()}-${hex()}${hex()}${hex()}`;\n}\n\n/**\n * The `run/edit` command for resubmitting `sourceId` with `text`, anchored on\n * the host's own parent. `null` when the anchor cannot be resolved from the\n * replicated state.\n */\nexport function buildDeepAgentEditCommand({\n extras,\n sourceId,\n text,\n}: {\n extras: DeepAgentStatewireExtras;\n sourceId: string;\n text: string;\n}): StatewireThread.RunEditCommand | null {\n const runAnchorMessageId = resolveWireAnchorMessageId({\n wireMessages: extras.state?.messages,\n sourceId,\n hasOlder: readWindow(extras)?.hasOlder ?? false,\n });\n if (runAnchorMessageId === undefined) return null;\n return {\n type: 'run/edit',\n runId: extras.runs.runId ?? mintCommandId(),\n message: { id: mintCommandId(), role: 'user', parts: [{ type: 'text', text }] },\n sourceId,\n runAnchorMessageId,\n };\n}\n\n/**\n * Whether a windowed statewire thread has older history than it has loaded:\n * `null` while that is not known yet (the head interest has not resolved; a\n * remembered transcript can show from cache before it does), and `false` for\n * a thread that is not a statewire thread, so a reader looking for an older\n * message waits for the window, or stops.\n */\nexport function deepAgentHistoryHasOlder(extras: unknown): boolean | null {\n const typed = readDeepAgentStatewireExtras(extras);\n if (typed === null) return false;\n return readWindow(typed)?.hasOlder ?? null;\n}\n\n/**\n * State one more page of older history for a windowed thread: the next page\n * past the loaded window's oldest message. Resolves `true` once the host has\n * mounted it (it answers the statement after mounting), `false` when there is\n * nothing older, the thread is not a windowed statewire thread, or no page can\n * be stated. For a reader that needs a message below the loaded window — a\n * search result revealing an older turn of a long conversation — and pages\n * until it renders.\n */\nexport async function loadOlderDeepAgentHistoryPage({\n readExtras,\n pageSize,\n}: {\n readExtras: () => DeepAgentStatewireExtras | null;\n pageSize?: number;\n}): Promise<boolean> {\n const extras = readExtras();\n if (!extras?.invoke) return false;\n const window = readWindow(extras);\n if (window === null || !window.hasOlder) return false;\n const requests = pageInterestRequests(window, pageSize);\n if (requests === null) return false;\n await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [requests]);\n return true;\n}\n\n/**\n * Pages of older history stated while looking for an edited message's host\n * parent. Each page is up to a window's worth of messages, so an edit reaches\n * this many windows below what was loaded before it gives up.\n */\nexport const MAX_EDIT_ANCHOR_HISTORY_LOADS = 5;\n\n/**\n * The anchored `run/edit` for `sourceId`, stating older page interests one at\n * a time when the message's parent lies below the loaded window. Every page\n * is read back from the live documents (`readExtras` — the host mounts them\n * before it answers the statement), so the anchor is always the host's own\n * parent id.\n *\n * Resolves `null` — the caller's cue for the library's composer path, whose\n * failure it reports — when the thread is not a statewire thread, the host has\n * not settled the message, the history is exhausted without it, or the page\n * budget ({@link MAX_EDIT_ANCHOR_HISTORY_LOADS}) is spent. A synchronous\n * resolution never awaits: the first read either anchors at once or decides\n * that pages are needed.\n */\nexport async function resolveDeepAgentEditCommand({\n readExtras,\n sourceId,\n text,\n pageSize,\n maxHistoryLoads = MAX_EDIT_ANCHOR_HISTORY_LOADS,\n}: {\n /** The live thread extras — re-read per page so each anchor attempt sees the widened window. */\n readExtras: () => DeepAgentStatewireExtras | null;\n sourceId: string;\n text: string;\n pageSize?: number;\n maxHistoryLoads?: number;\n}): Promise<StatewireThread.RunEditCommand | null> {\n for (let page = 0; page <= maxHistoryLoads; page += 1) {\n const extras = readExtras();\n if (!extras) return null;\n const command = buildDeepAgentEditCommand({ extras, sourceId, text });\n if (command) return command;\n if (page === maxHistoryLoads || !extras.invoke) return null;\n const window = readWindow(extras);\n if (!isSourceBelowLoadedWindow({ wireMessages: extras.state?.messages, sourceId, window })) {\n return null;\n }\n const requests = window === null ? null : pageInterestRequests(window, pageSize);\n if (requests === null) return null;\n await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [requests]);\n }\n return null;\n}\n"],"names":["hasStatewireThreadExtras","readDeepAgentMessageWindow","pageInterestRequests","DEEP_AGENT_INTEREST_PROTOCOL"],"mappings":"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"}
|
|
@@ -113,6 +113,14 @@ export declare function buildDeepAgentEditCommand({ extras, sourceId, text, }: {
|
|
|
113
113
|
sourceId: string;
|
|
114
114
|
text: string;
|
|
115
115
|
}): StatewireThread.RunEditCommand | null;
|
|
116
|
+
/**
|
|
117
|
+
* Whether a windowed statewire thread has older history than it has loaded:
|
|
118
|
+
* `null` while that is not known yet (the head interest has not resolved; a
|
|
119
|
+
* remembered transcript can show from cache before it does), and `false` for
|
|
120
|
+
* a thread that is not a statewire thread, so a reader looking for an older
|
|
121
|
+
* message waits for the window, or stops.
|
|
122
|
+
*/
|
|
123
|
+
export declare function deepAgentHistoryHasOlder(extras: unknown): boolean | null;
|
|
116
124
|
/**
|
|
117
125
|
* State one more page of older history for a windowed thread: the next page
|
|
118
126
|
* past the loaded window's oldest message. Resolves `true` once the host has
|
|
@@ -57,6 +57,25 @@ function buildDeepAgentEditCommand({
|
|
|
57
57
|
runAnchorMessageId
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
function deepAgentHistoryHasOlder(extras) {
|
|
61
|
+
var _a;
|
|
62
|
+
const typed = readDeepAgentStatewireExtras(extras);
|
|
63
|
+
if (typed === null) return false;
|
|
64
|
+
return ((_a = readWindow(typed)) == null ? void 0 : _a.hasOlder) ?? null;
|
|
65
|
+
}
|
|
66
|
+
async function loadOlderDeepAgentHistoryPage({
|
|
67
|
+
readExtras,
|
|
68
|
+
pageSize
|
|
69
|
+
}) {
|
|
70
|
+
const extras = readExtras();
|
|
71
|
+
if (!(extras == null ? void 0 : extras.invoke)) return false;
|
|
72
|
+
const window = readWindow(extras);
|
|
73
|
+
if (window === null || !window.hasOlder) return false;
|
|
74
|
+
const requests = pageInterestRequests(window, pageSize);
|
|
75
|
+
if (requests === null) return false;
|
|
76
|
+
await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, "set", [requests]);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
60
79
|
const MAX_EDIT_ANCHOR_HISTORY_LOADS = 5;
|
|
61
80
|
async function resolveDeepAgentEditCommand({
|
|
62
81
|
readExtras,
|
|
@@ -85,7 +104,9 @@ async function resolveDeepAgentEditCommand({
|
|
|
85
104
|
export {
|
|
86
105
|
MAX_EDIT_ANCHOR_HISTORY_LOADS,
|
|
87
106
|
buildDeepAgentEditCommand,
|
|
107
|
+
deepAgentHistoryHasOlder,
|
|
88
108
|
isSourceBelowLoadedWindow,
|
|
109
|
+
loadOlderDeepAgentHistoryPage,
|
|
89
110
|
readDeepAgentStatewireExtras,
|
|
90
111
|
resolveDeepAgentEditCommand,
|
|
91
112
|
resolveWireAnchorMessageId
|