@kortix/sdk 0.13.4 → 0.13.7
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 +63 -6
- package/dist/browser/cache/idb-sync-cache.d.ts +19 -1
- package/dist/browser/cache/idb-sync-cache.d.ts.map +1 -1
- package/dist/browser/cache/idb-sync-cache.js +39 -4
- package/dist/browser/cache/idb-sync-cache.js.map +1 -1
- package/dist/browser/session-sync/server-transcript-mirror.d.ts +82 -0
- package/dist/browser/session-sync/server-transcript-mirror.d.ts.map +1 -0
- package/dist/browser/session-sync/server-transcript-mirror.js +143 -0
- package/dist/browser/session-sync/server-transcript-mirror.js.map +1 -0
- package/dist/browser/session-sync/session-sync-registry.d.ts +11 -1
- package/dist/browser/session-sync/session-sync-registry.d.ts.map +1 -1
- package/dist/browser/session-sync/session-sync-registry.js +117 -9
- package/dist/browser/session-sync/session-sync-registry.js.map +1 -1
- package/dist/browser/session-sync/visibility.d.ts +19 -0
- package/dist/browser/session-sync/visibility.d.ts.map +1 -0
- package/dist/browser/session-sync/visibility.js +16 -0
- package/dist/browser/session-sync/visibility.js.map +1 -0
- package/dist/browser/stores/sandbox-connection-store.d.ts +13 -1
- package/dist/browser/stores/sandbox-connection-store.d.ts.map +1 -1
- package/dist/browser/stores/sandbox-connection-store.js +6 -3
- package/dist/browser/stores/sandbox-connection-store.js.map +1 -1
- package/dist/browser/stores/session-working-store.d.ts +10 -5
- package/dist/browser/stores/session-working-store.d.ts.map +1 -1
- package/dist/browser/stores/session-working-store.js +32 -8
- package/dist/browser/stores/session-working-store.js.map +1 -1
- package/dist/browser/stores/sync-store.d.ts +56 -2
- package/dist/browser/stores/sync-store.d.ts.map +1 -1
- package/dist/browser/stores/sync-store.js +682 -402
- package/dist/browser/stores/sync-store.js.map +1 -1
- package/dist/core/auth/app-viewer.d.ts +57 -0
- package/dist/core/auth/app-viewer.d.ts.map +1 -0
- package/dist/core/auth/app-viewer.js +82 -0
- package/dist/core/auth/app-viewer.js.map +1 -0
- package/dist/core/auth/session.d.ts +45 -0
- package/dist/core/auth/session.d.ts.map +1 -0
- package/dist/core/auth/session.js +113 -0
- package/dist/core/auth/session.js.map +1 -0
- package/dist/core/client/kortix.d.ts +37 -29
- package/dist/core/client/kortix.d.ts.map +1 -1
- package/dist/core/client/kortix.js +61 -8
- package/dist/core/client/kortix.js.map +1 -1
- package/dist/core/http/config.d.ts +4 -2
- package/dist/core/http/config.d.ts.map +1 -1
- package/dist/core/http/config.js.map +1 -1
- package/dist/core/http/opencode-errors.d.ts +17 -0
- package/dist/core/http/opencode-errors.d.ts.map +1 -1
- package/dist/core/http/opencode-errors.js +25 -0
- package/dist/core/http/opencode-errors.js.map +1 -1
- package/dist/core/rest/platform-client/auth.d.ts +120 -0
- package/dist/core/rest/platform-client/auth.d.ts.map +1 -0
- package/dist/core/rest/platform-client/auth.js +98 -0
- package/dist/core/rest/platform-client/auth.js.map +1 -0
- package/dist/core/rest/platform-client/host-boundary.d.ts +5 -0
- package/dist/core/rest/platform-client/host-boundary.d.ts.map +1 -1
- package/dist/core/rest/platform-client/host-boundary.js.map +1 -1
- package/dist/core/rest/projects-client/accounts.d.ts +57 -0
- package/dist/core/rest/projects-client/accounts.d.ts.map +1 -1
- package/dist/core/rest/projects-client/accounts.js +31 -0
- package/dist/core/rest/projects-client/accounts.js.map +1 -1
- package/dist/core/rest/projects-client/apps.d.ts +9 -0
- package/dist/core/rest/projects-client/apps.d.ts.map +1 -1
- package/dist/core/rest/projects-client/apps.js.map +1 -1
- package/dist/core/rest/projects-client/billing.d.ts +2 -0
- package/dist/core/rest/projects-client/billing.d.ts.map +1 -1
- package/dist/core/rest/projects-client/billing.js.map +1 -1
- package/dist/core/rest/projects-client/channels.d.ts +0 -3
- package/dist/core/rest/projects-client/channels.d.ts.map +1 -1
- package/dist/core/rest/projects-client/channels.js +0 -6
- package/dist/core/rest/projects-client/channels.js.map +1 -1
- package/dist/core/rest/projects-client/connectors.d.ts +104 -19
- package/dist/core/rest/projects-client/connectors.d.ts.map +1 -1
- package/dist/core/rest/projects-client/connectors.js +56 -2
- package/dist/core/rest/projects-client/connectors.js.map +1 -1
- package/dist/core/rest/projects-client/index.d.ts +1 -2
- package/dist/core/rest/projects-client/index.d.ts.map +1 -1
- package/dist/core/rest/projects-client/index.js +1 -2
- package/dist/core/rest/projects-client/index.js.map +1 -1
- package/dist/core/rest/projects-client/oauth-clients.d.ts +47 -0
- package/dist/core/rest/projects-client/oauth-clients.d.ts.map +1 -0
- package/dist/core/rest/projects-client/oauth-clients.js +33 -0
- package/dist/core/rest/projects-client/oauth-clients.js.map +1 -0
- package/dist/core/rest/projects-client/projects.d.ts +23 -6
- package/dist/core/rest/projects-client/projects.d.ts.map +1 -1
- package/dist/core/rest/projects-client/projects.js +6 -3
- package/dist/core/rest/projects-client/projects.js.map +1 -1
- package/dist/core/rest/projects-client/session-sandbox.d.ts +52 -0
- package/dist/core/rest/projects-client/session-sandbox.d.ts.map +1 -1
- package/dist/core/rest/projects-client/session-sandbox.js.map +1 -1
- package/dist/core/rest/projects-client/sessions.d.ts +142 -34
- package/dist/core/rest/projects-client/sessions.d.ts.map +1 -1
- package/dist/core/rest/projects-client/sessions.js +48 -19
- package/dist/core/rest/projects-client/sessions.js.map +1 -1
- package/dist/core/runtime/attachment-part.d.ts +26 -0
- package/dist/core/runtime/attachment-part.d.ts.map +1 -0
- package/dist/core/runtime/attachment-part.js +41 -0
- package/dist/core/runtime/attachment-part.js.map +1 -0
- package/dist/core/runtime/client.d.ts +4 -2
- package/dist/core/runtime/client.d.ts.map +1 -1
- package/dist/core/runtime/client.js +3 -2
- package/dist/core/runtime/client.js.map +1 -1
- package/dist/core/runtime/opencode-v2-event-aliases.d.ts +184 -0
- package/dist/core/runtime/opencode-v2-event-aliases.d.ts.map +1 -0
- package/dist/core/runtime/opencode-v2-event-aliases.js +2 -0
- package/dist/core/runtime/opencode-v2-event-aliases.js.map +1 -0
- package/dist/core/runtime/pty.d.ts +11 -1
- package/dist/core/runtime/pty.d.ts.map +1 -1
- package/dist/core/runtime/pty.js +19 -6
- package/dist/core/runtime/pty.js.map +1 -1
- package/dist/core/session/connection.d.ts +64 -0
- package/dist/core/session/connection.d.ts.map +1 -0
- package/dist/core/session/connection.js +36 -0
- package/dist/core/session/connection.js.map +1 -0
- package/dist/core/session/index.d.ts +2 -0
- package/dist/core/session/index.d.ts.map +1 -1
- package/dist/core/session/index.js +2 -0
- package/dist/core/session/index.js.map +1 -1
- package/dist/core/session/open-bundle.d.ts +110 -0
- package/dist/core/session/open-bundle.d.ts.map +1 -0
- package/dist/core/session/open-bundle.js +183 -0
- package/dist/core/session/open-bundle.js.map +1 -0
- package/dist/core/session/poll-owner.d.ts +35 -0
- package/dist/core/session/poll-owner.d.ts.map +1 -0
- package/dist/core/session/poll-owner.js +90 -0
- package/dist/core/session/poll-owner.js.map +1 -0
- package/dist/core/session/preview-auth.d.ts +25 -0
- package/dist/core/session/preview-auth.d.ts.map +1 -1
- package/dist/core/session/preview-auth.js +31 -1
- package/dist/core/session/preview-auth.js.map +1 -1
- package/dist/core/session/preview-config.d.ts +26 -0
- package/dist/core/session/preview-config.d.ts.map +1 -0
- package/dist/core/session/preview-config.js +95 -0
- package/dist/core/session/preview-config.js.map +1 -0
- package/dist/core/session/preview-options.d.ts +25 -0
- package/dist/core/session/preview-options.d.ts.map +1 -0
- package/dist/core/session/preview-options.js +45 -0
- package/dist/core/session/preview-options.js.map +1 -0
- package/dist/core/session/preview.d.ts +8 -3
- package/dist/core/session/preview.d.ts.map +1 -1
- package/dist/core/session/preview.js +11 -4
- package/dist/core/session/preview.js.map +1 -1
- package/dist/core/session/rewind.d.ts +67 -24
- package/dist/core/session/rewind.d.ts.map +1 -1
- package/dist/core/session/rewind.js +74 -26
- package/dist/core/session/rewind.js.map +1 -1
- package/dist/core/session/server-store/active.d.ts +10 -12
- package/dist/core/session/server-store/active.d.ts.map +1 -1
- package/dist/core/session/server-store/active.js +11 -9
- package/dist/core/session/server-store/active.js.map +1 -1
- package/dist/core/session/server-store/url-helpers.d.ts +11 -2
- package/dist/core/session/server-store/url-helpers.d.ts.map +1 -1
- package/dist/core/session/server-store/url-helpers.js +11 -2
- package/dist/core/session/server-store/url-helpers.js.map +1 -1
- package/dist/core/session/static-file-preview.d.ts +50 -0
- package/dist/core/session/static-file-preview.d.ts.map +1 -0
- package/dist/core/session/static-file-preview.js +91 -0
- package/dist/core/session/static-file-preview.js.map +1 -0
- package/dist/core/session/url.d.ts +25 -2
- package/dist/core/session/url.d.ts.map +1 -1
- package/dist/core/session/url.js +87 -11
- package/dist/core/session/url.js.map +1 -1
- package/dist/core/session/wake-escalation.d.ts +188 -0
- package/dist/core/session/wake-escalation.d.ts.map +1 -0
- package/dist/core/session/wake-escalation.js +241 -0
- package/dist/core/session/wake-escalation.js.map +1 -0
- package/dist/core/session/working.d.ts +107 -2
- package/dist/core/session/working.d.ts.map +1 -1
- package/dist/core/session/working.js +206 -7
- package/dist/core/session/working.js.map +1 -1
- package/dist/core/session-sync/durable-log.d.ts +96 -0
- package/dist/core/session-sync/durable-log.d.ts.map +1 -0
- package/dist/core/session-sync/durable-log.js +94 -0
- package/dist/core/session-sync/durable-log.js.map +1 -0
- package/dist/core/session-sync/fragment.d.ts +27 -0
- package/dist/core/session-sync/fragment.d.ts.map +1 -0
- package/dist/core/session-sync/fragment.js +24 -0
- package/dist/core/session-sync/fragment.js.map +1 -0
- package/dist/core/session-sync/session-sync-controller.d.ts +117 -11
- package/dist/core/session-sync/session-sync-controller.d.ts.map +1 -1
- package/dist/core/session-sync/session-sync-controller.js +278 -26
- package/dist/core/session-sync/session-sync-controller.js.map +1 -1
- package/dist/core/stream/keepalive.d.ts +17 -0
- package/dist/core/stream/keepalive.d.ts.map +1 -0
- package/dist/core/stream/keepalive.js +19 -0
- package/dist/core/stream/keepalive.js.map +1 -0
- package/dist/core/turns/grouping.d.ts.map +1 -1
- package/dist/core/turns/grouping.js +73 -23
- package/dist/core/turns/grouping.js.map +1 -1
- package/dist/core/turns/types.d.ts +5 -0
- package/dist/core/turns/types.d.ts.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/dist/kortix.esm.min.js +25 -25
- package/dist/kortix.global.js +25 -25
- package/dist/node/app-viewer.d.ts +41 -0
- package/dist/node/app-viewer.d.ts.map +1 -0
- package/dist/node/app-viewer.js +150 -0
- package/dist/node/app-viewer.js.map +1 -0
- package/dist/node/auth.d.ts +84 -0
- package/dist/node/auth.d.ts.map +1 -0
- package/dist/node/auth.js +414 -0
- package/dist/node/auth.js.map +1 -0
- package/dist/node/server.d.ts +19 -0
- package/dist/node/server.d.ts.map +1 -1
- package/dist/node/server.js +13 -1
- package/dist/node/server.js.map +1 -1
- package/dist/platform/auth-core.d.ts +13 -1
- package/dist/platform/auth-core.d.ts.map +1 -1
- package/dist/platform/auth-core.js +13 -1
- package/dist/platform/auth-core.js.map +1 -1
- package/dist/react/bedrock-invokable.d.ts +58 -0
- package/dist/react/bedrock-invokable.d.ts.map +1 -0
- package/dist/react/bedrock-invokable.js +83 -0
- package/dist/react/bedrock-invokable.js.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +7 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/model-flatten.d.ts +14 -1
- package/dist/react/model-flatten.d.ts.map +1 -1
- package/dist/react/model-flatten.js +3 -4
- package/dist/react/model-flatten.js.map +1 -1
- package/dist/react/opencode.d.ts +3 -1
- package/dist/react/opencode.d.ts.map +1 -1
- package/dist/react/opencode.js +5 -1
- package/dist/react/opencode.js.map +1 -1
- package/dist/react/provider-selection.d.ts +58 -0
- package/dist/react/provider-selection.d.ts.map +1 -1
- package/dist/react/provider-selection.js +253 -2
- package/dist/react/provider-selection.js.map +1 -1
- package/dist/react/session-start-giveup.d.ts +12 -0
- package/dist/react/session-start-giveup.d.ts.map +1 -0
- package/dist/react/session-start-giveup.js +13 -0
- package/dist/react/session-start-giveup.js.map +1 -0
- package/dist/react/session-title-sync.d.ts.map +1 -1
- package/dist/react/session-title-sync.js +15 -5
- package/dist/react/session-title-sync.js.map +1 -1
- package/dist/react/sign-in-with-kortix.d.ts +346 -0
- package/dist/react/sign-in-with-kortix.d.ts.map +1 -0
- package/dist/react/sign-in-with-kortix.js +65 -0
- package/dist/react/sign-in-with-kortix.js.map +1 -0
- package/dist/react/use-admin-accounts.d.ts +36 -1
- package/dist/react/use-admin-accounts.d.ts.map +1 -1
- package/dist/react/use-admin-accounts.js +19 -0
- package/dist/react/use-admin-accounts.js.map +1 -1
- package/dist/react/use-gateway-routing-policy.d.ts +1 -0
- package/dist/react/use-gateway-routing-policy.d.ts.map +1 -1
- package/dist/react/use-gateway-routing-policy.js +12 -1
- package/dist/react/use-gateway-routing-policy.js.map +1 -1
- package/dist/react/use-kortix-app-viewer.d.ts +13 -0
- package/dist/react/use-kortix-app-viewer.d.ts.map +1 -0
- package/dist/react/use-kortix-app-viewer.js +29 -0
- package/dist/react/use-kortix-app-viewer.js.map +1 -0
- package/dist/react/use-model-defaults.d.ts +7 -0
- package/dist/react/use-model-defaults.d.ts.map +1 -1
- package/dist/react/use-model-defaults.js +7 -1
- package/dist/react/use-model-defaults.js.map +1 -1
- package/dist/react/use-model-enablement.d.ts.map +1 -1
- package/dist/react/use-model-enablement.js +5 -1
- package/dist/react/use-model-enablement.js.map +1 -1
- package/dist/react/use-model-store.d.ts +31 -0
- package/dist/react/use-model-store.d.ts.map +1 -1
- package/dist/react/use-model-store.js +31 -2
- package/dist/react/use-model-store.js.map +1 -1
- package/dist/react/use-opencode-events/handle-event.d.ts.map +1 -1
- package/dist/react/use-opencode-events/handle-event.js +26 -0
- package/dist/react/use-opencode-events/handle-event.js.map +1 -1
- package/dist/react/use-opencode-events/helpers.d.ts +49 -5
- package/dist/react/use-opencode-events/helpers.d.ts.map +1 -1
- package/dist/react/use-opencode-events/helpers.js +54 -6
- package/dist/react/use-opencode-events/helpers.js.map +1 -1
- package/dist/react/use-opencode-events/index.d.ts.map +1 -1
- package/dist/react/use-opencode-events/index.js +80 -8
- package/dist/react/use-opencode-events/index.js.map +1 -1
- package/dist/react/use-opencode-events/rehydrate-targets.d.ts +18 -0
- package/dist/react/use-opencode-events/rehydrate-targets.d.ts.map +1 -0
- package/dist/react/use-opencode-events/rehydrate-targets.js +20 -0
- package/dist/react/use-opencode-events/rehydrate-targets.js.map +1 -0
- package/dist/react/use-opencode-events/stream-revival.d.ts +37 -0
- package/dist/react/use-opencode-events/stream-revival.d.ts.map +1 -0
- package/dist/react/use-opencode-events/stream-revival.js +53 -0
- package/dist/react/use-opencode-events/stream-revival.js.map +1 -0
- package/dist/react/use-opencode-events/use-event-stream-refs.d.ts.map +1 -1
- package/dist/react/use-opencode-events/use-event-stream-refs.js +14 -2
- package/dist/react/use-opencode-events/use-event-stream-refs.js.map +1 -1
- package/dist/react/use-opencode-local.d.ts.map +1 -1
- package/dist/react/use-opencode-local.js +27 -8
- package/dist/react/use-opencode-local.js.map +1 -1
- package/dist/react/use-opencode-pty.d.ts +7 -1
- package/dist/react/use-opencode-pty.d.ts.map +1 -1
- package/dist/react/use-opencode-pty.js +6 -2
- package/dist/react/use-opencode-pty.js.map +1 -1
- package/dist/react/use-opencode-sessions/agents.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/agents.js +13 -2
- package/dist/react/use-opencode-sessions/agents.js.map +1 -1
- package/dist/react/use-opencode-sessions/commands.d.ts +12 -0
- package/dist/react/use-opencode-sessions/commands.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/commands.js +15 -3
- package/dist/react/use-opencode-sessions/commands.js.map +1 -1
- package/dist/react/use-opencode-sessions/index.d.ts +1 -0
- package/dist/react/use-opencode-sessions/index.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/index.js +1 -0
- package/dist/react/use-opencode-sessions/index.js.map +1 -1
- package/dist/react/use-opencode-sessions/keys.d.ts +8 -0
- package/dist/react/use-opencode-sessions/keys.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/keys.js +8 -0
- package/dist/react/use-opencode-sessions/keys.js.map +1 -1
- package/dist/react/use-opencode-sessions/providers.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/providers.js +56 -5
- package/dist/react/use-opencode-sessions/providers.js.map +1 -1
- package/dist/react/use-opencode-sessions/shared.d.ts +23 -0
- package/dist/react/use-opencode-sessions/shared.d.ts.map +1 -1
- package/dist/react/use-opencode-sessions/shared.js +31 -0
- package/dist/react/use-opencode-sessions/shared.js.map +1 -1
- package/dist/react/use-opencode-sessions/vcs.d.ts +28 -0
- package/dist/react/use-opencode-sessions/vcs.d.ts.map +1 -0
- package/dist/react/use-opencode-sessions/vcs.js +42 -0
- package/dist/react/use-opencode-sessions/vcs.js.map +1 -0
- package/dist/react/use-poll-owner.d.ts +14 -0
- package/dist/react/use-poll-owner.d.ts.map +1 -0
- package/dist/react/use-poll-owner.js +32 -0
- package/dist/react/use-poll-owner.js.map +1 -0
- package/dist/react/use-project-llm-gateway.d.ts +27 -0
- package/dist/react/use-project-llm-gateway.d.ts.map +1 -0
- package/dist/react/use-project-llm-gateway.js +41 -0
- package/dist/react/use-project-llm-gateway.js.map +1 -0
- package/dist/react/use-project-models.d.ts +9 -0
- package/dist/react/use-project-models.d.ts.map +1 -1
- package/dist/react/use-project-models.js +24 -1
- package/dist/react/use-project-models.js.map +1 -1
- package/dist/react/use-runtime-reconnect.d.ts +11 -0
- package/dist/react/use-runtime-reconnect.d.ts.map +1 -1
- package/dist/react/use-runtime-reconnect.js +34 -3
- package/dist/react/use-runtime-reconnect.js.map +1 -1
- package/dist/react/use-session-prompts.d.ts +53 -3
- package/dist/react/use-session-prompts.d.ts.map +1 -1
- package/dist/react/use-session-prompts.js +133 -21
- package/dist/react/use-session-prompts.js.map +1 -1
- package/dist/react/use-session-send.d.ts +17 -0
- package/dist/react/use-session-send.d.ts.map +1 -1
- package/dist/react/use-session-send.js +54 -5
- package/dist/react/use-session-send.js.map +1 -1
- package/dist/react/use-session-start-give-up.d.ts +20 -0
- package/dist/react/use-session-start-give-up.d.ts.map +1 -0
- package/dist/react/use-session-start-give-up.js +60 -0
- package/dist/react/use-session-start-give-up.js.map +1 -0
- package/dist/react/use-session-sync.d.ts +35 -1
- package/dist/react/use-session-sync.d.ts.map +1 -1
- package/dist/react/use-session-sync.js +135 -53
- package/dist/react/use-session-sync.js.map +1 -1
- package/dist/react/use-session-working.d.ts +55 -0
- package/dist/react/use-session-working.d.ts.map +1 -1
- package/dist/react/use-session-working.js +114 -13
- package/dist/react/use-session-working.js.map +1 -1
- package/dist/react/use-session.d.ts +13 -13
- package/dist/react/use-session.d.ts.map +1 -1
- package/dist/react/use-session.js +74 -53
- package/dist/react/use-session.js.map +1 -1
- package/dist/react/use-static-file-preview.d.ts +12 -0
- package/dist/react/use-static-file-preview.d.ts.map +1 -0
- package/dist/react/use-static-file-preview.js +119 -0
- package/dist/react/use-static-file-preview.js.map +1 -0
- package/dist/react/use-wake-escalation.d.ts +51 -0
- package/dist/react/use-wake-escalation.d.ts.map +1 -0
- package/dist/react/use-wake-escalation.js +62 -0
- package/dist/react/use-wake-escalation.js.map +1 -0
- package/package.json +6 -4
- package/dist/browser/session-sync/session-transcript-cache.d.ts +0 -79
- package/dist/browser/session-sync/session-transcript-cache.d.ts.map +0 -1
- package/dist/browser/session-sync/session-transcript-cache.js +0 -91
- package/dist/browser/session-sync/session-transcript-cache.js.map +0 -1
- package/dist/core/rest/projects-client/public-voice-join.d.ts +0 -59
- package/dist/core/rest/projects-client/public-voice-join.d.ts.map +0 -1
- package/dist/core/rest/projects-client/public-voice-join.js +0 -64
- package/dist/core/rest/projects-client/public-voice-join.js.map +0 -1
- package/dist/core/rest/projects-client/transcription.d.ts +0 -5
- package/dist/core/rest/projects-client/transcription.d.ts.map +0 -1
- package/dist/core/rest/projects-client/transcription.js +0 -14
- package/dist/core/rest/projects-client/transcription.js.map +0 -1
|
@@ -1,15 +1,50 @@
|
|
|
1
1
|
import type { Message, Part, SessionStatus } from '@opencode-ai/sdk/v2/client';
|
|
2
|
+
export declare const SESSION_SYNC_PAGE_SIZE = 100;
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
* and the step size when the transcript pulls older history.
|
|
4
|
+
* The FIRST page — the one the user waits on.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Time to first paint is bytes, not messages. Measured on a heavy session
|
|
7
|
+
* (essentia, 2026-08-24, a run with hundreds of image reads whose parts carried
|
|
8
|
+
* base64 — BEFORE the attachment bytes were stripped from the list):
|
|
9
|
+
*
|
|
10
|
+
* message?limit=50 -> 8,228 kB 30.39 s
|
|
11
|
+
* message?limit=50 -> 24,460 kB 48.76 s
|
|
12
|
+
* message?limit=50 -> 20,284 kB 35.74 s
|
|
13
|
+
* message?limit=50 -> 25,125 kB 29.23 s
|
|
14
|
+
*
|
|
15
|
+
* That was 165-500 kB PER MESSAGE, and it drove the first page down to 20.
|
|
16
|
+
* With the bytes gone (`stripInlineAttachmentBytes`, same session, same day)
|
|
17
|
+
* a message is ~2-7 kB: `limit=20` measured 132 kB, an older page of 50
|
|
18
|
+
* measured 345-420 kB. At that weight a small first page is a false economy —
|
|
19
|
+
* on a normal viewport 20 messages do not fill the screen, so the top sentinel
|
|
20
|
+
* pulled two more pages before the reader touched anything, and three round
|
|
21
|
+
* trips (each paying a CORS preflight, one measured at 3.34 s) replaced one.
|
|
22
|
+
*
|
|
23
|
+
* Fifty fills the view. Older pages go to a hundred: by then the reader is
|
|
24
|
+
* scrolling deliberately, a spinner is honest, and fewer round trips wins.
|
|
25
|
+
*
|
|
26
|
+
* NOTE this is not what fixed the blank transcript. That was structural:
|
|
27
|
+
* `hydrate` ran only after a multi-page backward walk, so NOTHING rendered at
|
|
28
|
+
* any page size — a smaller limit would have meant MORE sequential round trips
|
|
29
|
+
* before that single paint, and a longer blank. The walk is gone (see
|
|
30
|
+
* `loadTail`); this only makes the first paint lighter.
|
|
31
|
+
*/
|
|
32
|
+
export declare const SESSION_SYNC_TAIL_PAGE_SIZE = 50;
|
|
33
|
+
/**
|
|
34
|
+
* How far back the tail read will walk to complete a turn before it stops and
|
|
35
|
+
* leaves the rest to "load older".
|
|
36
|
+
*
|
|
37
|
+
* The walk exists so an assistant message is not rendered above its own prompt.
|
|
38
|
+
* It had no ceiling, and a session whose last turn is thousands of messages —
|
|
39
|
+
* an agent run with hundreds of tool calls — therefore paged the WHOLE session
|
|
40
|
+
* 50 at a time, serially, through the sandbox proxy, before painting anything.
|
|
41
|
+
* The read returns 200 the entire time, which is why this looked like a bug in
|
|
42
|
+
* rendering rather than in loading.
|
|
43
|
+
*
|
|
44
|
+
* 10 pages = 500 messages, far past any honest turn, and the cursor survives so
|
|
45
|
+
* nothing becomes unreachable.
|
|
11
46
|
*/
|
|
12
|
-
export declare const
|
|
47
|
+
export declare const MAX_TURN_BACKFILL_PAGES = 10;
|
|
13
48
|
export type SessionSyncFreshness = 'idle' | 'loading' | 'fresh' | 'stale' | 'error';
|
|
14
49
|
export interface SessionSyncMessage {
|
|
15
50
|
info: Message;
|
|
@@ -43,7 +78,16 @@ export interface SessionSyncScheduler {
|
|
|
43
78
|
setTimeout?: (handler: () => void, timeoutMs: number) => unknown;
|
|
44
79
|
clearTimeout?: (handle: unknown) => void;
|
|
45
80
|
}
|
|
46
|
-
export type SessionSyncReason = 'initial' | 'poll' | 'sse-gap' | 'compaction' | 'session-error' | 'send-recovery'
|
|
81
|
+
export type SessionSyncReason = 'initial' | 'poll' | 'sse-gap' | 'compaction' | 'session-error' | 'send-recovery'
|
|
82
|
+
/** The turn ended. The last read of a busy session, and the one that catches
|
|
83
|
+
* a final answer whose closing frames the stream never delivered. */
|
|
84
|
+
| 'turn-end'
|
|
85
|
+
/** The tab became visible again. A backgrounded tab is throttled, not
|
|
86
|
+
* notified, so return is a moment to re-read rather than to assume. */
|
|
87
|
+
| 'visible'
|
|
88
|
+
/** The transcript was evicted while detached and the live stream refilled it
|
|
89
|
+
* from the middle — see `transcriptIsFragment`. */
|
|
90
|
+
| 'eviction' | 'manual';
|
|
47
91
|
export interface SessionSyncTelemetryEvent {
|
|
48
92
|
operation: 'tail' | 'older';
|
|
49
93
|
reason: SessionSyncReason;
|
|
@@ -53,10 +97,17 @@ export interface SessionSyncTelemetryEvent {
|
|
|
53
97
|
}
|
|
54
98
|
export interface SessionSyncControllerOptions {
|
|
55
99
|
sessionId: string;
|
|
100
|
+
/**
|
|
101
|
+
* Read one bounded page. `signal` is the controller's own — aborted on
|
|
102
|
+
* `destroy()` (a scope reset destroys the controller), so a page loader that
|
|
103
|
+
* forwards it into `fetch` cancels a read the tab has navigated away from,
|
|
104
|
+
* and a superseded read can never hydrate the store. Passing it is optional;
|
|
105
|
+
* a loader that ignores it still works, it just cannot be cancelled.
|
|
106
|
+
*/
|
|
56
107
|
loadPage: (request: {
|
|
57
108
|
limit: number;
|
|
58
109
|
before?: string;
|
|
59
|
-
}) => Promise<SessionSyncPage>;
|
|
110
|
+
}, signal?: AbortSignal) => Promise<SessionSyncPage>;
|
|
60
111
|
/**
|
|
61
112
|
* @deprecated Never called. The liveness poll no longer reads status: `GET
|
|
62
113
|
* .../turn` is the status authority, and this controller's own `setBusy` is
|
|
@@ -78,8 +129,23 @@ export interface SessionSyncControllerOptions {
|
|
|
78
129
|
onTelemetry?: (event: SessionSyncTelemetryEvent) => void;
|
|
79
130
|
scheduler?: SessionSyncScheduler;
|
|
80
131
|
livenessIntervalMs?: number;
|
|
132
|
+
/**
|
|
133
|
+
* Max age of the last tail read while the session is busy, whatever the
|
|
134
|
+
* stream delivers. Default 30s.
|
|
135
|
+
*
|
|
136
|
+
* The quiet-based poll trusts `noteActivity`: any transcript frame renews it.
|
|
137
|
+
* A DEGRADED stream — events lost at the source or the edge, connection
|
|
138
|
+
* alive, a trickle still arriving — therefore postponed the tail read
|
|
139
|
+
* indefinitely while the transcript diverged arbitrarily far from the
|
|
140
|
+
* runtime (prod, 2026-08-26: content minutes behind mid-run, repaired only
|
|
141
|
+
* by reload). Frames arriving proves the wire is up; it proves nothing about
|
|
142
|
+
* the frames that never arrived. This is the bound on how long that
|
|
143
|
+
* difference can go unchecked: one tail page per interval, and against a
|
|
144
|
+
* healthy stream the hydrate is a no-op.
|
|
145
|
+
*/
|
|
146
|
+
verifyIntervalMs?: number;
|
|
81
147
|
}
|
|
82
|
-
export interface HttpSessionSyncControllerOptions extends Pick<SessionSyncControllerOptions, 'sessionId' | 'hydrate' | 'markLoaded' | 'setStatus' | 'onTelemetry' | 'scheduler' | 'livenessIntervalMs'> {
|
|
148
|
+
export interface HttpSessionSyncControllerOptions extends Pick<SessionSyncControllerOptions, 'sessionId' | 'hydrate' | 'markLoaded' | 'setStatus' | 'onTelemetry' | 'scheduler' | 'livenessIntervalMs' | 'verifyIntervalMs'> {
|
|
83
149
|
baseUrl: string;
|
|
84
150
|
getToken?: () => string | null | Promise<string | null>;
|
|
85
151
|
fetch?: SessionSyncFetch;
|
|
@@ -89,6 +155,21 @@ type HttpSessionSyncSourceOptions = Pick<HttpSessionSyncControllerOptions, 'base
|
|
|
89
155
|
export declare function createHttpSessionSyncPageLoader(options: HttpSessionSyncSourceOptions): SessionSyncControllerOptions['loadPage'];
|
|
90
156
|
export declare function loadHttpSessionHistory(options: HttpSessionSyncSourceOptions): Promise<SessionSyncMessage[]>;
|
|
91
157
|
export declare function createHttpSessionSyncController(options: HttpSessionSyncControllerOptions): SessionSyncController;
|
|
158
|
+
/**
|
|
159
|
+
* Every message in the session, oldest first.
|
|
160
|
+
*
|
|
161
|
+
* The order is the SERVER's: `MessageV2.page()` orders by `time_created`, so
|
|
162
|
+
* each page arrives already ordered and is concatenated untouched. Paging
|
|
163
|
+
* walks BACKWARDS (page 1 is the newest tail, `before` steps into history), so
|
|
164
|
+
* the transcript is the pages in reverse fetch order.
|
|
165
|
+
*
|
|
166
|
+
* This used to re-sort the union by `info.id.localeCompare(...)`. That was
|
|
167
|
+
* wrong twice over: message ids no longer ascend with time at all (OpenCode
|
|
168
|
+
* 1.18.15 retired that invariant — turn exit reads
|
|
169
|
+
* `lastAssistant.parentID === lastUser.id`, and `MessageV2.latest()` orders by
|
|
170
|
+
* `time.created`), and `localeCompare` is not byte order, so it disagreed with
|
|
171
|
+
* every other id comparison in the product on the same data.
|
|
172
|
+
*/
|
|
92
173
|
export declare function loadCompleteSessionHistory(loadPage: SessionSyncControllerOptions['loadPage']): Promise<SessionSyncMessage[]>;
|
|
93
174
|
/**
|
|
94
175
|
* Owns bounded session history synchronization without depending on React,
|
|
@@ -98,6 +179,12 @@ export declare class SessionSyncController {
|
|
|
98
179
|
private readonly options;
|
|
99
180
|
private readonly scheduler;
|
|
100
181
|
private readonly livenessIntervalMs;
|
|
182
|
+
private readonly verifyIntervalMs;
|
|
183
|
+
/** When the last tail read was ISSUED — see `verifyIntervalMs`. */
|
|
184
|
+
private lastTailReadAt;
|
|
185
|
+
/** Consecutive failed tail reads, for the retry backoff. Reset by success. */
|
|
186
|
+
private retryAttempt;
|
|
187
|
+
private tailRetryTimer;
|
|
101
188
|
private snapshot;
|
|
102
189
|
private nextCursor;
|
|
103
190
|
private knownUserMessageIds;
|
|
@@ -108,6 +195,13 @@ export declare class SessionSyncController {
|
|
|
108
195
|
private lastActivityAt;
|
|
109
196
|
private listeners;
|
|
110
197
|
private destroyed;
|
|
198
|
+
/**
|
|
199
|
+
* One controller-lifetime signal, threaded into every read. A scope reset
|
|
200
|
+
* (registry `resetSessionSyncControllersForSession`) or unmount destroys this
|
|
201
|
+
* controller, `destroy()` aborts this, and the in-flight read cancels — so a
|
|
202
|
+
* navigated-away or superseded read frees its socket and can never hydrate.
|
|
203
|
+
*/
|
|
204
|
+
private readonly abortController;
|
|
111
205
|
constructor(options: SessionSyncControllerOptions);
|
|
112
206
|
getSnapshot: () => SessionSyncSnapshot;
|
|
113
207
|
subscribe: (listener: () => void) => (() => void);
|
|
@@ -126,6 +220,18 @@ export declare class SessionSyncController {
|
|
|
126
220
|
setBusy(isBusy: boolean): void;
|
|
127
221
|
destroy(): void;
|
|
128
222
|
private loadTail;
|
|
223
|
+
/**
|
|
224
|
+
* Come back for a read that lost.
|
|
225
|
+
*
|
|
226
|
+
* Without this a session got exactly ONE chance to load, and whether it took
|
|
227
|
+
* it depended on a race with the sandbox's boot. Losing that race left the
|
|
228
|
+
* page on "Waking the agent" with no exit but the health probe — the least
|
|
229
|
+
* reliable signal in the system — or a manual reload.
|
|
230
|
+
*
|
|
231
|
+
* Backoff so a genuinely dead box costs little, capped so a box that comes
|
|
232
|
+
* back is picked up promptly.
|
|
233
|
+
*/
|
|
234
|
+
private scheduleTailRetry;
|
|
129
235
|
private loadPage;
|
|
130
236
|
private loadCompleteOlderTurn;
|
|
131
237
|
private loadCompleteTurn;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-sync-controller.d.ts","sourceRoot":"","sources":["../../../src/core/session-sync/session-sync-controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"session-sync-controller.d.ts","sourceRoot":"","sources":["../../../src/core/session-sync/session-sync-controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SandboxNotReadyError, isSandboxNotReadyError } from '../http/opencode-errors.js';
|
|
2
|
+
import { isAbortError } from '../http/abort-error.js';
|
|
1
3
|
/**
|
|
2
4
|
* Messages per bounded read — the newest-first window a session opens with,
|
|
3
5
|
* and the step size when the transcript pulls older history.
|
|
@@ -8,17 +10,75 @@
|
|
|
8
10
|
* fully loaded and never pull at all, while a long one still opens bounded
|
|
9
11
|
* instead of dragging its entire history over the sandbox proxy.
|
|
10
12
|
*/
|
|
11
|
-
|
|
13
|
+
/** First retry delay after a failed tail read. */
|
|
14
|
+
const TAIL_RETRY_BASE_MS = 1_000;
|
|
15
|
+
/** Ceiling for the retry backoff — a box that comes back is picked up within it. */
|
|
16
|
+
const TAIL_RETRY_MAX_MS = 15_000;
|
|
17
|
+
export const SESSION_SYNC_PAGE_SIZE = 100;
|
|
18
|
+
/**
|
|
19
|
+
* The FIRST page — the one the user waits on.
|
|
20
|
+
*
|
|
21
|
+
* Time to first paint is bytes, not messages. Measured on a heavy session
|
|
22
|
+
* (essentia, 2026-08-24, a run with hundreds of image reads whose parts carried
|
|
23
|
+
* base64 — BEFORE the attachment bytes were stripped from the list):
|
|
24
|
+
*
|
|
25
|
+
* message?limit=50 -> 8,228 kB 30.39 s
|
|
26
|
+
* message?limit=50 -> 24,460 kB 48.76 s
|
|
27
|
+
* message?limit=50 -> 20,284 kB 35.74 s
|
|
28
|
+
* message?limit=50 -> 25,125 kB 29.23 s
|
|
29
|
+
*
|
|
30
|
+
* That was 165-500 kB PER MESSAGE, and it drove the first page down to 20.
|
|
31
|
+
* With the bytes gone (`stripInlineAttachmentBytes`, same session, same day)
|
|
32
|
+
* a message is ~2-7 kB: `limit=20` measured 132 kB, an older page of 50
|
|
33
|
+
* measured 345-420 kB. At that weight a small first page is a false economy —
|
|
34
|
+
* on a normal viewport 20 messages do not fill the screen, so the top sentinel
|
|
35
|
+
* pulled two more pages before the reader touched anything, and three round
|
|
36
|
+
* trips (each paying a CORS preflight, one measured at 3.34 s) replaced one.
|
|
37
|
+
*
|
|
38
|
+
* Fifty fills the view. Older pages go to a hundred: by then the reader is
|
|
39
|
+
* scrolling deliberately, a spinner is honest, and fewer round trips wins.
|
|
40
|
+
*
|
|
41
|
+
* NOTE this is not what fixed the blank transcript. That was structural:
|
|
42
|
+
* `hydrate` ran only after a multi-page backward walk, so NOTHING rendered at
|
|
43
|
+
* any page size — a smaller limit would have meant MORE sequential round trips
|
|
44
|
+
* before that single paint, and a longer blank. The walk is gone (see
|
|
45
|
+
* `loadTail`); this only makes the first paint lighter.
|
|
46
|
+
*/
|
|
47
|
+
export const SESSION_SYNC_TAIL_PAGE_SIZE = 50;
|
|
48
|
+
/**
|
|
49
|
+
* How far back the tail read will walk to complete a turn before it stops and
|
|
50
|
+
* leaves the rest to "load older".
|
|
51
|
+
*
|
|
52
|
+
* The walk exists so an assistant message is not rendered above its own prompt.
|
|
53
|
+
* It had no ceiling, and a session whose last turn is thousands of messages —
|
|
54
|
+
* an agent run with hundreds of tool calls — therefore paged the WHOLE session
|
|
55
|
+
* 50 at a time, serially, through the sandbox proxy, before painting anything.
|
|
56
|
+
* The read returns 200 the entire time, which is why this looked like a bug in
|
|
57
|
+
* rendering rather than in loading.
|
|
58
|
+
*
|
|
59
|
+
* 10 pages = 500 messages, far past any honest turn, and the cursor survives so
|
|
60
|
+
* nothing becomes unreachable.
|
|
61
|
+
*/
|
|
62
|
+
export const MAX_TURN_BACKFILL_PAGES = 10;
|
|
12
63
|
export function createHttpSessionSyncPageLoader(options) {
|
|
13
64
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
14
65
|
const baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
15
|
-
return async ({ limit, before }) => {
|
|
66
|
+
return async ({ limit, before }, signal) => {
|
|
16
67
|
const query = new URLSearchParams({ limit: String(limit) });
|
|
17
68
|
if (before)
|
|
18
69
|
query.set('before', before);
|
|
19
70
|
const token = await options.getToken?.();
|
|
20
|
-
const response = await fetchImpl(`${baseUrl}/session/${encodeURIComponent(options.sessionId)}/message?${query}`, {
|
|
71
|
+
const response = await fetchImpl(`${baseUrl}/session/${encodeURIComponent(options.sessionId)}/message?${query}`, {
|
|
72
|
+
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
|
73
|
+
...(signal ? { signal } : {}),
|
|
74
|
+
});
|
|
21
75
|
if (!response.ok) {
|
|
76
|
+
// A 503 from the sandbox proxy is the box waking, not a failure — throw
|
|
77
|
+
// the retryable, "loading" error so the controller keeps polling instead
|
|
78
|
+
// of painting an error state over a session that is about to load.
|
|
79
|
+
if (response.status === 503) {
|
|
80
|
+
throw new SandboxNotReadyError(`session ${response.status}`);
|
|
81
|
+
}
|
|
22
82
|
throw new Error(`Session synchronization failed: ${response.status}`);
|
|
23
83
|
}
|
|
24
84
|
return {
|
|
@@ -39,8 +99,23 @@ export function createHttpSessionSyncController(options) {
|
|
|
39
99
|
loadPage: createHttpSessionSyncPageLoader(options),
|
|
40
100
|
});
|
|
41
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Every message in the session, oldest first.
|
|
104
|
+
*
|
|
105
|
+
* The order is the SERVER's: `MessageV2.page()` orders by `time_created`, so
|
|
106
|
+
* each page arrives already ordered and is concatenated untouched. Paging
|
|
107
|
+
* walks BACKWARDS (page 1 is the newest tail, `before` steps into history), so
|
|
108
|
+
* the transcript is the pages in reverse fetch order.
|
|
109
|
+
*
|
|
110
|
+
* This used to re-sort the union by `info.id.localeCompare(...)`. That was
|
|
111
|
+
* wrong twice over: message ids no longer ascend with time at all (OpenCode
|
|
112
|
+
* 1.18.15 retired that invariant — turn exit reads
|
|
113
|
+
* `lastAssistant.parentID === lastUser.id`, and `MessageV2.latest()` orders by
|
|
114
|
+
* `time.created`), and `localeCompare` is not byte order, so it disagreed with
|
|
115
|
+
* every other id comparison in the product on the same data.
|
|
116
|
+
*/
|
|
42
117
|
export async function loadCompleteSessionHistory(loadPage) {
|
|
43
|
-
const
|
|
118
|
+
const pages = [];
|
|
44
119
|
const cursors = new Set();
|
|
45
120
|
let before;
|
|
46
121
|
do {
|
|
@@ -48,9 +123,7 @@ export async function loadCompleteSessionHistory(loadPage) {
|
|
|
48
123
|
limit: SESSION_SYNC_PAGE_SIZE,
|
|
49
124
|
...(before ? { before } : {}),
|
|
50
125
|
});
|
|
51
|
-
|
|
52
|
-
messages.set(message.info.id, message);
|
|
53
|
-
}
|
|
126
|
+
pages.push(page.messages);
|
|
54
127
|
before = page.nextCursor;
|
|
55
128
|
if (before && cursors.has(before)) {
|
|
56
129
|
throw new Error(`Session history cursor repeated: ${before}`);
|
|
@@ -58,7 +131,20 @@ export async function loadCompleteSessionHistory(loadPage) {
|
|
|
58
131
|
if (before)
|
|
59
132
|
cursors.add(before);
|
|
60
133
|
} while (before);
|
|
61
|
-
|
|
134
|
+
// Oldest page last out of the loop, so walk the pages backwards. A message
|
|
135
|
+
// repeated across two overlapping pages keeps the OLDER page's copy and its
|
|
136
|
+
// older position — the same copy the id-keyed Map used to end up holding.
|
|
137
|
+
const ordered = [];
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
for (let index = pages.length - 1; index >= 0; index--) {
|
|
140
|
+
for (const message of pages[index]) {
|
|
141
|
+
if (seen.has(message.info.id))
|
|
142
|
+
continue;
|
|
143
|
+
seen.add(message.info.id);
|
|
144
|
+
ordered.push(message);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return ordered;
|
|
62
148
|
}
|
|
63
149
|
const defaultScheduler = {
|
|
64
150
|
now: Date.now,
|
|
@@ -75,6 +161,12 @@ export class SessionSyncController {
|
|
|
75
161
|
options;
|
|
76
162
|
scheduler;
|
|
77
163
|
livenessIntervalMs;
|
|
164
|
+
verifyIntervalMs;
|
|
165
|
+
/** When the last tail read was ISSUED — see `verifyIntervalMs`. */
|
|
166
|
+
lastTailReadAt;
|
|
167
|
+
/** Consecutive failed tail reads, for the retry backoff. Reset by success. */
|
|
168
|
+
retryAttempt = 0;
|
|
169
|
+
tailRetryTimer;
|
|
78
170
|
snapshot = {
|
|
79
171
|
freshness: 'idle',
|
|
80
172
|
hasOlder: false,
|
|
@@ -89,11 +181,20 @@ export class SessionSyncController {
|
|
|
89
181
|
lastActivityAt;
|
|
90
182
|
listeners = new Set();
|
|
91
183
|
destroyed = false;
|
|
184
|
+
/**
|
|
185
|
+
* One controller-lifetime signal, threaded into every read. A scope reset
|
|
186
|
+
* (registry `resetSessionSyncControllersForSession`) or unmount destroys this
|
|
187
|
+
* controller, `destroy()` aborts this, and the in-flight read cancels — so a
|
|
188
|
+
* navigated-away or superseded read frees its socket and can never hydrate.
|
|
189
|
+
*/
|
|
190
|
+
abortController = new AbortController();
|
|
92
191
|
constructor(options) {
|
|
93
192
|
this.options = options;
|
|
94
193
|
this.scheduler = options.scheduler ?? defaultScheduler;
|
|
95
194
|
this.livenessIntervalMs = options.livenessIntervalMs ?? 10_000;
|
|
195
|
+
this.verifyIntervalMs = options.verifyIntervalMs ?? 30_000;
|
|
96
196
|
this.lastActivityAt = this.scheduler.now();
|
|
197
|
+
this.lastTailReadAt = this.scheduler.now();
|
|
97
198
|
}
|
|
98
199
|
getSnapshot = () => this.snapshot;
|
|
99
200
|
subscribe = (listener) => {
|
|
@@ -110,8 +211,13 @@ export class SessionSyncController {
|
|
|
110
211
|
return Promise.resolve();
|
|
111
212
|
if (this.tailRequest)
|
|
112
213
|
return this.tailRequest;
|
|
214
|
+
// `loading` covers the first read AND a not-ready retry, so a waking box
|
|
215
|
+
// does not flip to `stale` between attempts; a session that already has a
|
|
216
|
+
// fresh transcript revalidates as `stale`.
|
|
113
217
|
this.update({
|
|
114
|
-
freshness: this.snapshot.freshness === 'idle'
|
|
218
|
+
freshness: this.snapshot.freshness === 'idle' || this.snapshot.freshness === 'loading'
|
|
219
|
+
? 'loading'
|
|
220
|
+
: 'stale',
|
|
115
221
|
});
|
|
116
222
|
this.tailRequest = this.loadTail(reason).finally(() => {
|
|
117
223
|
this.tailRequest = undefined;
|
|
@@ -156,7 +262,21 @@ export class SessionSyncController {
|
|
|
156
262
|
*/
|
|
157
263
|
setBusy(isBusy) {
|
|
158
264
|
if (!isBusy) {
|
|
265
|
+
// The turn is over — and that is exactly when the transcript is most
|
|
266
|
+
// likely to be short. A stream that dropped its last frames leaves the
|
|
267
|
+
// browser holding a truncated answer while the runtime holds the whole
|
|
268
|
+
// one, and stopping the poll here used to make that state PERMANENT:
|
|
269
|
+
// nothing read the tail again until the session was reopened. Reported
|
|
270
|
+
// from a live self-host (2026-08-24): an 8m13s turn finished in the
|
|
271
|
+
// runtime's own terminal while the tab still showed a spinner under a
|
|
272
|
+
// half-written answer.
|
|
273
|
+
//
|
|
274
|
+
// One bounded read, only for a session that was actually busy, so an
|
|
275
|
+
// idle session churns nothing.
|
|
276
|
+
const wasBusy = this.livenessTimer !== undefined;
|
|
159
277
|
this.stopLivenessTimer();
|
|
278
|
+
if (wasBusy && !this.destroyed)
|
|
279
|
+
void this.reconcile('turn-end');
|
|
160
280
|
return;
|
|
161
281
|
}
|
|
162
282
|
if (this.livenessTimer !== undefined)
|
|
@@ -165,14 +285,50 @@ export class SessionSyncController {
|
|
|
165
285
|
this.livenessTimer = this.scheduler.setInterval(() => void this.checkLiveness(), this.livenessIntervalMs);
|
|
166
286
|
}
|
|
167
287
|
destroy() {
|
|
288
|
+
// `destroyed` FIRST: `stopLivenessTimer` is also reached through
|
|
289
|
+
// `setBusy(false)`, which now fires a turn-end read, and a controller being
|
|
290
|
+
// torn down must not start a request it can never hydrate.
|
|
168
291
|
this.destroyed = true;
|
|
169
292
|
this.stopLivenessTimer();
|
|
293
|
+
if (this.tailRetryTimer !== undefined) {
|
|
294
|
+
this.cancelTimer(this.tailRetryTimer);
|
|
295
|
+
this.tailRetryTimer = undefined;
|
|
296
|
+
}
|
|
297
|
+
// Cancel any in-flight read so it frees its socket and cannot hydrate a
|
|
298
|
+
// controller that is being torn down.
|
|
299
|
+
this.abortController.abort();
|
|
170
300
|
this.listeners.clear();
|
|
171
301
|
}
|
|
172
302
|
async loadTail(reason) {
|
|
303
|
+
// Stamped at ISSUE: any tail read — initial, poll, gap, visible — is a
|
|
304
|
+
// verification, so the busy-verification cadence counts from the last
|
|
305
|
+
// attempt rather than piling on top of reads other reasons already ran.
|
|
306
|
+
this.lastTailReadAt = this.scheduler.now();
|
|
173
307
|
try {
|
|
174
|
-
|
|
175
|
-
|
|
308
|
+
// ONE PAGE. Render it. This is what OpenCode's own client does, and the
|
|
309
|
+
// reason we now do it too is measured:
|
|
310
|
+
//
|
|
311
|
+
// message?limit=50 200 8,228 kB 30.39 s
|
|
312
|
+
// message?limit=50 200 24,460 kB 48.76 s
|
|
313
|
+
// message?limit=50&before=.. 200 20,284 kB 35.74 s
|
|
314
|
+
// message?limit=50&before=.. 200 25,125 kB 29.23 s
|
|
315
|
+
// -> 78,097 kB transferred, finish 3.8 min, NOTHING on screen
|
|
316
|
+
//
|
|
317
|
+
// (essentia, 2026-08-24, a run with hundreds of image reads: fifty
|
|
318
|
+
// messages weigh 8-25 MB because the parts carry the image bytes.)
|
|
319
|
+
//
|
|
320
|
+
// There used to be a backward WALK here that kept fetching pages until
|
|
321
|
+
// every assistant message had its parent user message in hand, so an
|
|
322
|
+
// assistant reply could never render above its own prompt — and `hydrate`
|
|
323
|
+
// ran only after the walk finished. On a long turn that walk is the whole
|
|
324
|
+
// session, serially, through the sandbox proxy, with an empty thread the
|
|
325
|
+
// entire time. A cosmetic guarantee at the top edge of the window is not
|
|
326
|
+
// worth minutes of blank screen.
|
|
327
|
+
//
|
|
328
|
+
// The window may therefore start on an assistant message whose prompt is
|
|
329
|
+
// one page up. That is exactly what OpenCode shows, and `loadOlder` —
|
|
330
|
+
// which the user drives — still completes the turn when they scroll.
|
|
331
|
+
const page = await this.loadPage('tail', reason);
|
|
176
332
|
if (this.destroyed)
|
|
177
333
|
return;
|
|
178
334
|
this.rememberUserMessages(page.messages);
|
|
@@ -181,24 +337,68 @@ export class SessionSyncController {
|
|
|
181
337
|
this.setCursor(page.nextCursor);
|
|
182
338
|
}
|
|
183
339
|
this.update({ freshness: 'fresh' });
|
|
340
|
+
// SUCCESS ONLY. `markLoaded` used to sit in a `finally`, so a read that
|
|
341
|
+
// FAILED still told the store this session was loaded — and the store's
|
|
342
|
+
// implementation of that plants an empty message list. A first read that
|
|
343
|
+
// lost to a waking box, a 503 from the proxy, or a flapping probe was
|
|
344
|
+
// therefore RECORDED as "this session has no messages", and the UI
|
|
345
|
+
// painted an empty conversation over a session that had plenty. Nothing
|
|
346
|
+
// came back for it either: the mount had already run, and the liveness
|
|
347
|
+
// poll only turns on while a session is working.
|
|
348
|
+
//
|
|
349
|
+
// A successful read of zero messages still marks loaded — that is a fact
|
|
350
|
+
// about the session. A failed read is not a fact about anything.
|
|
351
|
+
this.options.markLoaded();
|
|
352
|
+
this.retryAttempt = 0;
|
|
184
353
|
}
|
|
185
|
-
catch {
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (this.destroyed)
|
|
356
|
+
return;
|
|
357
|
+
// A superseded/cancelled read is not a failure and never hydrates — it
|
|
358
|
+
// must not paint an error over a live transcript, and it must not retry.
|
|
359
|
+
if (isAbortError(error) || this.abortController.signal.aborted)
|
|
360
|
+
return;
|
|
361
|
+
// A sandbox that is still waking is a RETRYABLE, "loading" state, not a
|
|
362
|
+
// fault: the box may come up any second, so keep polling and keep the UI
|
|
363
|
+
// on its loader. Only a genuine failure earns `error`. NEVER an
|
|
364
|
+
// empty-`fresh` — `markLoaded` above ran only on success, so a failed
|
|
365
|
+
// read never records the session as an empty transcript.
|
|
366
|
+
this.update({ freshness: isSandboxNotReadyError(error) ? 'loading' : 'error' });
|
|
367
|
+
this.scheduleTailRetry(reason);
|
|
193
368
|
}
|
|
194
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Come back for a read that lost.
|
|
372
|
+
*
|
|
373
|
+
* Without this a session got exactly ONE chance to load, and whether it took
|
|
374
|
+
* it depended on a race with the sandbox's boot. Losing that race left the
|
|
375
|
+
* page on "Waking the agent" with no exit but the health probe — the least
|
|
376
|
+
* reliable signal in the system — or a manual reload.
|
|
377
|
+
*
|
|
378
|
+
* Backoff so a genuinely dead box costs little, capped so a box that comes
|
|
379
|
+
* back is picked up promptly.
|
|
380
|
+
*/
|
|
381
|
+
scheduleTailRetry(reason) {
|
|
382
|
+
if (this.destroyed || this.tailRetryTimer !== undefined)
|
|
383
|
+
return;
|
|
384
|
+
const delay = Math.min(TAIL_RETRY_BASE_MS * 2 ** this.retryAttempt, TAIL_RETRY_MAX_MS);
|
|
385
|
+
this.retryAttempt += 1;
|
|
386
|
+
this.tailRetryTimer = this.startTimer(() => {
|
|
387
|
+
this.tailRetryTimer = undefined;
|
|
388
|
+
if (this.destroyed)
|
|
389
|
+
return;
|
|
390
|
+
void this.reconcile(reason);
|
|
391
|
+
}, delay);
|
|
392
|
+
}
|
|
195
393
|
async loadPage(operation, reason, before) {
|
|
196
394
|
const startedAt = this.scheduler.now();
|
|
197
395
|
try {
|
|
198
396
|
const page = await this.options.loadPage({
|
|
199
|
-
|
|
397
|
+
// The tail is what someone is waiting for; an older page is what they
|
|
398
|
+
// asked for. Different budgets — see SESSION_SYNC_TAIL_PAGE_SIZE.
|
|
399
|
+
limit: operation === 'tail' ? SESSION_SYNC_TAIL_PAGE_SIZE : SESSION_SYNC_PAGE_SIZE,
|
|
200
400
|
...(before ? { before } : {}),
|
|
201
|
-
});
|
|
401
|
+
}, this.abortController.signal);
|
|
202
402
|
this.options.onTelemetry?.({
|
|
203
403
|
operation,
|
|
204
404
|
reason,
|
|
@@ -221,19 +421,46 @@ export class SessionSyncController {
|
|
|
221
421
|
}
|
|
222
422
|
async loadCompleteOlderTurn(before) {
|
|
223
423
|
const firstPage = await this.loadPage('older', 'manual', before);
|
|
224
|
-
|
|
424
|
+
// Commit each page as it lands, including the page that fails. The walk
|
|
425
|
+
// is up to 11 sequential reads (MAX_TURN_BACKFILL_PAGES + 1) and used to
|
|
426
|
+
// commit all of them in one `.then`, so a rejection on ANY page —
|
|
427
|
+
// including the loop's very first, right after `firstPage` — discarded
|
|
428
|
+
// every successful read and left `nextCursor` unmoved: nothing to resume
|
|
429
|
+
// from, so the retry replayed the identical walk. `onPage` existed for
|
|
430
|
+
// exactly this and was never passed.
|
|
431
|
+
return this.loadCompleteTurn(firstPage, 'older', 'manual', before, (partialPage) => {
|
|
432
|
+
// `rememberUserMessages` mutates the instance-level `knownUserMessageIds`
|
|
433
|
+
// Set (below), and `onPage` fires on a rejection too (the catch in
|
|
434
|
+
// `loadCompleteTurn`, right before it rethrows). So a walk that fails
|
|
435
|
+
// partway permanently records the user-message ids from every page read
|
|
436
|
+
// so far. On the retry, `loadCompleteTurn` seeds its local
|
|
437
|
+
// `knownUserMessageIds` from this now-larger instance Set, which can
|
|
438
|
+
// already satisfy the termination condition ("some assistant has a
|
|
439
|
+
// `parentID` not in `knownUserMessageIds`") from `firstPage` alone — the
|
|
440
|
+
// retry's `while` loop never runs, and `nextCursor` advances by one page
|
|
441
|
+
// instead of walking the rest of the turn. Accepted: the pages already
|
|
442
|
+
// landed in the store on the failed attempt via this same callback,
|
|
443
|
+
// which is the fix this method exists for.
|
|
444
|
+
this.rememberUserMessages(partialPage.messages);
|
|
445
|
+
this.options.hydrate(partialPage.messages);
|
|
446
|
+
this.setCursor(partialPage.nextCursor);
|
|
447
|
+
});
|
|
225
448
|
}
|
|
226
|
-
async loadCompleteTurn(firstPage, operation, reason, initialCursor) {
|
|
449
|
+
async loadCompleteTurn(firstPage, operation, reason, initialCursor, onPage) {
|
|
227
450
|
const messages = [...firstPage.messages];
|
|
228
451
|
const knownUserMessageIds = new Set(this.knownUserMessageIds);
|
|
229
452
|
const seenCursors = new Set(initialCursor ? [initialCursor] : []);
|
|
230
453
|
let cursor = firstPage.nextCursor;
|
|
454
|
+
let pagesRead = 0;
|
|
231
455
|
for (const message of firstPage.messages) {
|
|
232
456
|
if (message.info.role === 'user') {
|
|
233
457
|
knownUserMessageIds.add(message.info.id);
|
|
234
458
|
}
|
|
235
459
|
}
|
|
236
460
|
while (cursor &&
|
|
461
|
+
// BOUNDED. Without a ceiling a turn of a few thousand messages paged the
|
|
462
|
+
// whole session before anything rendered — see MAX_TURN_BACKFILL_PAGES.
|
|
463
|
+
pagesRead < MAX_TURN_BACKFILL_PAGES &&
|
|
237
464
|
messages.some((message) => message.info.role === 'assistant' &&
|
|
238
465
|
Boolean(message.info.parentID) &&
|
|
239
466
|
!knownUserMessageIds.has(message.info.parentID))) {
|
|
@@ -241,7 +468,21 @@ export class SessionSyncController {
|
|
|
241
468
|
throw new Error(`Session history cursor repeated: ${cursor}`);
|
|
242
469
|
}
|
|
243
470
|
seenCursors.add(cursor);
|
|
244
|
-
|
|
471
|
+
let page;
|
|
472
|
+
try {
|
|
473
|
+
page = await this.loadPage(operation, reason, cursor);
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
// `messages` already holds every page read so far — firstPage
|
|
477
|
+
// (seeded before the loop) plus every completed iteration. Commit it
|
|
478
|
+
// before rethrowing so a rejection on THIS read (including the
|
|
479
|
+
// loop's very first) cannot drop what was already read.
|
|
480
|
+
onPage?.({ messages: [...messages], nextCursor: cursor });
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
if (this.destroyed)
|
|
484
|
+
return { messages, nextCursor: cursor };
|
|
485
|
+
pagesRead += 1;
|
|
245
486
|
messages.unshift(...page.messages);
|
|
246
487
|
for (const message of page.messages) {
|
|
247
488
|
if (message.info.role === 'user') {
|
|
@@ -249,6 +490,9 @@ export class SessionSyncController {
|
|
|
249
490
|
}
|
|
250
491
|
}
|
|
251
492
|
cursor = page.nextCursor;
|
|
493
|
+
// Repaint as each page lands, so a long turn fills in front of the user
|
|
494
|
+
// instead of withholding everything until the walk ends.
|
|
495
|
+
onPage?.({ messages: [...messages], nextCursor: cursor });
|
|
252
496
|
}
|
|
253
497
|
return { messages, nextCursor: cursor };
|
|
254
498
|
}
|
|
@@ -260,9 +504,17 @@ export class SessionSyncController {
|
|
|
260
504
|
}
|
|
261
505
|
}
|
|
262
506
|
async checkLiveness() {
|
|
263
|
-
if (this.destroyed
|
|
507
|
+
if (this.destroyed)
|
|
508
|
+
return;
|
|
509
|
+
const nowMs = this.scheduler.now();
|
|
510
|
+
const quiet = nowMs - this.lastActivityAt > this.livenessIntervalMs;
|
|
511
|
+
// `noteActivity` proves frames are ARRIVING, not that none were lost. A
|
|
512
|
+
// degraded stream that still delivers a trickle renewed the quiet timer
|
|
513
|
+
// forever while the transcript diverged — so a busy session re-reads the
|
|
514
|
+
// tail at `verifyIntervalMs` no matter how live the stream looks.
|
|
515
|
+
const verifyDue = nowMs - this.lastTailReadAt >= this.verifyIntervalMs;
|
|
516
|
+
if (!quiet && !verifyDue)
|
|
264
517
|
return;
|
|
265
|
-
}
|
|
266
518
|
// Reconcile the TAIL, and nothing else. This is the repair for a dropped
|
|
267
519
|
// SSE stream: the transcript catches up on messages the stream never
|
|
268
520
|
// delivered.
|