mixdog 1.4.1 → 1.4.2
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/package.json +3 -2
- package/scripts/build-shipped-history.mjs +115 -33
- package/scripts/gate-local.mjs +172 -61
- package/scripts/lib/gate-scheduler.mjs +138 -32
- package/scripts/lib/gate-timing-reporter.mjs +16 -0
- package/scripts/lib/run-node-tests.mjs +54 -29
- package/scripts/lib/scoped-temp.mjs +1 -0
- package/scripts/lib/test-failure-records.mjs +14 -1
- package/scripts/lib/test-failure-summary.mjs +16 -4
- package/scripts/lib/test-spawn-ledger.mjs +12 -3
- package/scripts/pptx-kit-index.mjs +5 -6
- package/scripts/suite-health.mjs +110 -28
- package/scripts/test.mjs +9 -14
- package/src/agents/advisor/AGENT.md +4 -4
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/explore/AGENT.md +22 -0
- package/src/agents/explore/agent.json +6 -0
- package/src/cli.mjs +7 -0
- package/src/defaults/agents.json +7 -0
- package/src/defaults/shipped-history.json +18 -2
- package/src/defaults/skills/browser-use/SKILL.md +4 -1
- package/src/defaults/skills/code-tidy/SKILL.md +71 -102
- package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
- package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
- package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
- package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
- package/src/defaults/skills/computer-use/SKILL.md +12 -2
- package/src/defaults/skills/goal-management/SKILL.md +1 -1
- package/src/defaults/skills/setup/references/actions.md +12 -6
- package/src/defaults/skills/setup/references/surfaces.md +2 -2
- package/src/defaults/skills/setup-guide/SKILL.md +5 -2
- package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
- package/src/defaults/skills/skill-creator/SKILL.md +85 -190
- package/src/defaults/skills/skill-creator/references/design.md +70 -162
- package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
- package/src/defaults/skills/test-suite/SKILL.md +194 -0
- package/src/defaults/skills/test-suite/references/case-management.md +51 -0
- package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
- package/src/defaults/skills/test-suite/references/reliability.md +49 -0
- package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
- package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
- package/src/defaults/skills/visualize/SKILL.md +6 -2
- package/src/lib/rules-builder.cjs +6 -30
- package/src/rules/lead/LEAD.md +3 -3
- package/src/rules/shared/20-research.md +3 -6
- package/src/rules/shared/40-editing.md +1 -0
- package/src/rules/shared/60-verification.md +10 -9
- package/src/rules/shared/70-delivery.md +4 -3
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
- package/src/runtime/agent/orchestrator/config.mjs +12 -7
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
- package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
- package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
- package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
- package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
- package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
- package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
- package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
- package/src/runtime/channels/lib/whisper-port.mjs +19 -10
- package/src/runtime/channels/lib/whisper-server.mjs +10 -1
- package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
- package/src/runtime/connectors/gmail-app-password.mjs +3 -3
- package/src/runtime/explore/explorer.mjs +93 -0
- package/src/runtime/explore/tool-defs.mjs +35 -0
- package/src/runtime/explore/tool.mjs +123 -0
- package/src/runtime/memory/index.mjs +4 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
- package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
- package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
- package/src/runtime/shared/agent-route-config.mjs +17 -2
- package/src/runtime/shared/atomic-file.mjs +103 -2
- package/src/runtime/shared/background-tasks.mjs +15 -6
- package/src/runtime/shared/connector-accounts.mjs +5 -5
- package/src/runtime/shared/connector-catalog.d.mts +11 -4
- package/src/runtime/shared/connector-catalog.mjs +60 -52
- package/src/runtime/shared/custom-provider-keys.mjs +20 -0
- package/src/runtime/shared/file-has-data.mjs +26 -2
- package/src/runtime/shared/file-lock.mjs +13 -15
- package/src/runtime/shared/git-launch.d.mts +9 -0
- package/src/runtime/shared/git-launch.mjs +213 -0
- package/src/runtime/shared/git-runner-worker.mjs +2 -1
- package/src/runtime/shared/llm/cost.mjs +6 -1
- package/src/runtime/shared/llm/http-agent.mjs +313 -29
- package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
- package/src/runtime/shared/llm/model-catalog.mjs +46 -6
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
- package/src/runtime/shared/llm/system-proxy.mjs +452 -0
- package/src/runtime/shared/provider-accounts.mjs +52 -2
- package/src/runtime/shared/staged-install-worker.mjs +5 -0
- package/src/runtime/shared/staged-update.mjs +47 -2
- package/src/runtime/shared/tool-surface.mjs +110 -36
- package/src/runtime/shared/tool-work-units.mjs +4 -0
- package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
- package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
- package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
- package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
- package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
- package/src/runtime/shared/worktrees/git.mjs +57 -0
- package/src/runtime/shared/worktrees/manager.mjs +205 -137
- package/src/runtime/tidy/install.mjs +95 -15
- package/src/runtime/tidy/languages.mjs +17 -15
- package/src/runtime/tidy/process.mjs +7 -4
- package/src/runtime/tidy/resolve.mjs +19 -16
- package/src/runtime/tidy/runners/biome.mjs +3 -3
- package/src/runtime/tidy/tool.mjs +1 -1
- package/src/runtime/tidy/worktree.mjs +10 -6
- package/src/runtime/visualize-bridge/client.mjs +4 -1
- package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
- package/src/runtime/web-search/index.mjs +1 -1
- package/src/session-runtime/agent-task-sessions.mjs +410 -353
- package/src/session-runtime/boot/tools.mjs +2 -0
- package/src/session-runtime/bot-status.mjs +103 -103
- package/src/session-runtime/connector-merge-migration.mjs +18 -15
- package/src/session-runtime/conversation-import/discovery.mjs +159 -40
- package/src/session-runtime/conversation-import/service.mjs +9 -3
- package/src/session-runtime/handoff-attachments.mjs +1 -1
- package/src/session-runtime/held-requests.mjs +3 -1
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
- package/src/session-runtime/lifecycle/teardown.mjs +1 -1
- package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
- package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
- package/src/session-runtime/mixy-heartbeat.mjs +1 -1
- package/src/session-runtime/provider-auth/login.mjs +2 -2
- package/src/session-runtime/resource-mcp-api.mjs +98 -35
- package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
- package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
- package/src/session-runtime/resource-mcp-servers.mjs +5 -4
- package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
- package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
- package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
- package/src/session-runtime/services/daemon-candidate.mjs +4 -1
- package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
- package/src/session-runtime/services/plugin-admin.mjs +10 -6
- package/src/session-runtime/services/provider-admin.mjs +281 -10
- package/src/session-runtime/session-turn-api.mjs +2 -0
- package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
- package/src/session-runtime/settings-system-api.mjs +1 -1
- package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
- package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
- package/src/session-runtime/setup-tool/executor.mjs +6 -2
- package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
- package/src/session-runtime/turn/agent-ops.mjs +23 -0
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
- package/src/session-runtime/workspace/activity-guards.mjs +176 -0
- package/src/session-runtime/workspace/session-view.mjs +149 -0
- package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
- package/src/session-runtime/workspace/transition-runner.mjs +188 -0
- package/src/session-runtime/workspace/transitions.mjs +108 -1066
- package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
- package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
- package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
- package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
- package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
- package/src/standalone/daemon-desktop-runtime.mjs +2 -0
- package/src/standalone/daemon-session-control.mjs +151 -136
- package/src/standalone/daemon.mjs +7 -0
- package/src/standalone/desktop-service-registry.mjs +9 -0
- package/src/standalone/local-session-runtime.mjs +5 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
- package/src/standalone/session-client.mjs +5 -1
- package/src/standalone/session-frame-stream.mjs +83 -11
- package/src/standalone/session-protocol.mjs +4 -0
- package/src/standalone/session-service/agent-tree.mjs +45 -9
- package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
- package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
- package/src/standalone/session-service/stored-reader.mjs +127 -40
- package/src/tui/App.jsx +154 -164
- package/src/tui/app/app-view/floating-panel.jsx +3 -0
- package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
- package/src/tui/app/clipboard.mjs +21 -10
- package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
- package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
- package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
- package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
- package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
- package/src/tui/app/provider-setup-picker.mjs +20 -13
- package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
- package/src/tui/app/slash-dispatch.mjs +1 -1
- package/src/tui/app/use-btw-panel.mjs +29 -0
- package/src/tui/app/use-context-panel-refresh.mjs +31 -0
- package/src/tui/app/use-model-catalog-cache.mjs +13 -10
- package/src/tui/app/use-mouse-input.mjs +11 -0
- package/src/tui/app/use-onboarding-start.mjs +8 -3
- package/src/tui/app/use-panel-transition.mjs +10 -1
- package/src/tui/app/use-pasted-buffers.mjs +28 -20
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
- package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
- package/src/tui/app/use-prompt-queue-history.mjs +3 -2
- package/src/tui/app/use-selection-focus.mjs +11 -2
- package/src/tui/app/use-selection-geometry.mjs +18 -9
- package/src/tui/app/use-selection-paint.mjs +4 -4
- package/src/tui/app/use-selection-stitch.mjs +3 -3
- package/src/tui/app/use-slash-palette.mjs +62 -0
- package/src/tui/app/use-terminal-chrome.mjs +2 -2
- package/src/tui/app/use-transcript-activity.mjs +47 -32
- package/src/tui/app/use-transcript-follow.mjs +12 -4
- package/src/tui/app/use-transcript-scroll.mjs +46 -17
- package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
- package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
- package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
- package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
- package/src/tui/app/use-transcript-window.mjs +21 -11
- package/src/tui/app/use-tui-ready.mjs +18 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
- package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
- package/src/tui/components/Picker.jsx +27 -3
- package/src/tui/components/SignInUrl.jsx +68 -0
- package/src/tui/components/TextEntryPanel.jsx +76 -46
- package/src/tui/components/sign-in-url-layout.mjs +24 -0
- package/src/tui/dist/index.mjs +1896 -1341
- package/src/tui/session/goal-turn-state.mjs +6 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/extensions.mjs +8 -0
- package/src/tui/session/session-api/intake/abort.mjs +2 -1
- package/src/tui/session/session-api/intake.mjs +2 -0
- package/src/tui/session/session-api/integrations.mjs +2 -2
- package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
- package/src/tui/session/session-api/settings.mjs +2 -2
- package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
- package/src/tui/session/transcript-spill.mjs +92 -4
- package/src/tui/session/transcript-store.mjs +2 -0
- package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
|
@@ -15,6 +15,13 @@ import { sanitizeForWire } from '../../session-wire-values.mjs';
|
|
|
15
15
|
* whatever their size: a window always shows something and paging always
|
|
16
16
|
* progresses. A viewport the rows do not fill pages further on its own. */
|
|
17
17
|
const TRANSCRIPT_WINDOW_MIN_ITEMS = 8;
|
|
18
|
+
/** The most rows one answer to a paged request (one naming the rows it holds,
|
|
19
|
+
* `transcriptHeld`) carries, whatever window it names: a window sent whole,
|
|
20
|
+
* or the rows one page reveals above the ones it holds. History itself has
|
|
21
|
+
* no limit; it pages in steps of at most this many. */
|
|
22
|
+
export const TRANSCRIPT_RESPONSE_MAX_ITEMS = 512;
|
|
23
|
+
/** A request naming no held rows keeps its earlier contract: a window of at
|
|
24
|
+
* most this many rows, answered whole. */
|
|
18
25
|
const TRANSCRIPT_WINDOW_MAX_ITEMS = 8_192;
|
|
19
26
|
// Deterministic restore ids: hist_<sessionId>_<messageIndex>_<part>.
|
|
20
27
|
const RESTORED_ITEM_ID = /^hist_.+_(\d+)_\d+$/;
|
|
@@ -31,18 +38,26 @@ function itemBytes(item) {
|
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
/** The window a read/subscribe asked for, or null for a legacy view.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
41
|
+
* A paged request (`paged`: it names the rows it holds, `transcriptHeld`)
|
|
42
|
+
* names how far back it wants history, without bound; what one answer
|
|
43
|
+
* carries is bounded by TRANSCRIPT_RESPONSE_MAX_ITEMS and the byte budget.
|
|
44
|
+
* Any other request keeps its earlier contract: at most
|
|
45
|
+
* TRANSCRIPT_WINDOW_MAX_ITEMS rows, answered whole, and `transcriptPageBase`
|
|
46
|
+
* names how many newest items the reader already holds — the byte budget
|
|
47
|
+
* then bounds only the older rows revealed above them. */
|
|
36
48
|
export function requestedTranscriptWindow(params) {
|
|
37
|
-
const
|
|
38
|
-
if (!Number.
|
|
49
|
+
const requested = Number(params?.transcriptItemLimit);
|
|
50
|
+
if (!Number.isSafeInteger(requested) || requested < 1) return null;
|
|
39
51
|
const budget = Number(params?.transcriptByteBudget);
|
|
52
|
+
const byteBudget = Number.isSafeInteger(budget) && budget > 0 ? budget : null;
|
|
53
|
+
if (requestedHeldTranscript(params)) return { limit: requested, byteBudget, pageBase: 0, paged: true };
|
|
54
|
+
const limit = Math.min(TRANSCRIPT_WINDOW_MAX_ITEMS, requested);
|
|
40
55
|
const pageBase = Number(params?.transcriptPageBase);
|
|
41
|
-
const cappedLimit = Math.min(TRANSCRIPT_WINDOW_MAX_ITEMS, limit);
|
|
42
56
|
return {
|
|
43
|
-
limit
|
|
44
|
-
byteBudget
|
|
45
|
-
pageBase: Number.isInteger(pageBase) && pageBase > 0 ? Math.min(pageBase,
|
|
57
|
+
limit,
|
|
58
|
+
byteBudget,
|
|
59
|
+
pageBase: Number.isInteger(pageBase) && pageBase > 0 ? Math.min(pageBase, limit) : 0,
|
|
60
|
+
paged: false,
|
|
46
61
|
};
|
|
47
62
|
}
|
|
48
63
|
|
|
@@ -57,17 +72,46 @@ export function requestedHeldTranscript(params) {
|
|
|
57
72
|
return { firstId: held.firstId, count, digest: held.digest };
|
|
58
73
|
}
|
|
59
74
|
|
|
75
|
+
/** Whether the window's newest `held.count` rows are exactly the rows the
|
|
76
|
+
* caller holds: same first id and, unless the caller already proved them
|
|
77
|
+
* (`proven`), the same content digest. */
|
|
78
|
+
export function holdsTranscriptTail(items, held, proven = false) {
|
|
79
|
+
const start = items.length - held.count;
|
|
80
|
+
if (start < 0 || items[start]?.id !== held.firstId) return false;
|
|
81
|
+
return proven || transcriptItemsDigest(start ? items.slice(start) : items) === held.digest;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Whether rows one answer carries are within its bounds: at most
|
|
85
|
+
* TRANSCRIPT_RESPONSE_MAX_ITEMS, and within `byteBudget` past
|
|
86
|
+
* TRANSCRIPT_WINDOW_MIN_ITEMS. */
|
|
87
|
+
function withinAnswerBounds(items, byteBudget, maxItems = TRANSCRIPT_RESPONSE_MAX_ITEMS) {
|
|
88
|
+
if (items.length > maxItems) return false;
|
|
89
|
+
return (
|
|
90
|
+
!byteBudget ||
|
|
91
|
+
items.length <= TRANSCRIPT_WINDOW_MIN_ITEMS ||
|
|
92
|
+
items.reduce((bytes, item) => bytes + itemBytes(item), 0) <= byteBudget
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
60
96
|
/** A FULL answer to a paged read, reduced to what the caller lacks: the rows
|
|
61
97
|
* above the ones it holds, the identity of its first held row, and every
|
|
62
|
-
* other field. Only when the window's tail is exactly those held rows
|
|
63
|
-
*
|
|
64
|
-
|
|
98
|
+
* other field. Only when the window's tail is exactly those held rows
|
|
99
|
+
* (holdsTranscriptTail; `proven` when the reader already verified them) and
|
|
100
|
+
* the rows above them fit one answer (withinAnswerBounds, `byteBudget`);
|
|
101
|
+
* otherwise the full answer stands. */
|
|
102
|
+
export function transcriptPageBody(result, held, { proven = false, byteBudget = null, pageOnly = false } = {}) {
|
|
65
103
|
const snapshot = result?.full;
|
|
66
104
|
if (!held || !snapshot || !Array.isArray(snapshot.items)) return result;
|
|
105
|
+
if (pageOnly) {
|
|
106
|
+
// Already only the (bounded) rows above the proven held ones.
|
|
107
|
+
const { full: _full, ...body } = result;
|
|
108
|
+
const { items, ...state } = snapshot;
|
|
109
|
+
return { ...body, page: { firstHeldId: held.firstId, heldCount: held.count, items, state } };
|
|
110
|
+
}
|
|
67
111
|
const items = snapshot.items;
|
|
112
|
+
if (!holdsTranscriptTail(items, held, proven)) return result;
|
|
68
113
|
const start = items.length - held.count;
|
|
69
|
-
if (
|
|
70
|
-
if (transcriptItemsDigest(start ? items.slice(start) : items) !== held.digest) return result;
|
|
114
|
+
if (!withinAnswerBounds(items.slice(0, start), byteBudget)) return result;
|
|
71
115
|
const { full: _full, ...body } = result;
|
|
72
116
|
const { items: _items, ...state } = snapshot;
|
|
73
117
|
return {
|
|
@@ -91,25 +135,44 @@ function budgetedStart(items, end, maxItems, byteBudget, minItems) {
|
|
|
91
135
|
return start;
|
|
92
136
|
}
|
|
93
137
|
|
|
94
|
-
/** First index of the kept tail: at most `limit` items
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
138
|
+
/** First index of the kept tail: at most `limit` items (never more than
|
|
139
|
+
* TRANSCRIPT_WINDOW_MAX_ITEMS). A first window spends `byteBudget` on the
|
|
140
|
+
* whole tail; a page keeps the `pageBase` newest items the reader holds and
|
|
141
|
+
* spends it on the rows revealed above them. Neither goes below
|
|
142
|
+
* TRANSCRIPT_WINDOW_MIN_ITEMS. */
|
|
98
143
|
function tailWindowStart(items, { limit, byteBudget = null, pageBase = 0 }) {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
144
|
+
const maxItems = Math.min(limit, TRANSCRIPT_WINDOW_MAX_ITEMS);
|
|
145
|
+
const held = Math.min(pageBase || 0, maxItems, items.length);
|
|
146
|
+
return budgetedStart(items, items.length - held, maxItems - held, byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The answer a stored window (at most `window.limit` newest rows) gives. A
|
|
150
|
+
* paged request: the `held` newest rows the caller is proven to hold, and
|
|
151
|
+
* above them at most TRANSCRIPT_RESPONSE_MAX_ITEMS more within the byte
|
|
152
|
+
* budget — or, proving none, a tail within the same bounds. Any other
|
|
153
|
+
* request: the whole window, byte-budgeted above its `pageBase`. */
|
|
154
|
+
export function clampStoredWindow(wire, window, held = 0) {
|
|
155
|
+
if (!window || !Array.isArray(wire?.items)) return wire;
|
|
156
|
+
const items = wire.items;
|
|
157
|
+
let start = 0;
|
|
158
|
+
if (window.paged) {
|
|
159
|
+
const maxItems = Math.min(window.limit - held, TRANSCRIPT_RESPONSE_MAX_ITEMS);
|
|
160
|
+
start = budgetedStart(items, items.length - held, maxItems, window.byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
|
|
161
|
+
} else if (window.byteBudget) {
|
|
162
|
+
start = tailWindowStart(items, { limit: items.length, byteBudget: window.byteBudget, pageBase: window.pageBase });
|
|
163
|
+
}
|
|
164
|
+
return start === 0 ? wire : { ...wire, items: items.slice(start), transcriptHasOlder: true };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A stored page (only the rows above the `held` ones a paged caller proved)
|
|
168
|
+
* cut to one answer: at most TRANSCRIPT_RESPONSE_MAX_ITEMS (and the window's
|
|
169
|
+
* limit) newest of them within the byte budget. */
|
|
170
|
+
export function clampStoredPage(wire, window, held) {
|
|
171
|
+
if (!Array.isArray(wire?.items)) return wire;
|
|
172
|
+
const items = wire.items;
|
|
173
|
+
const maxItems = Math.max(0, Math.min(window.limit - held, TRANSCRIPT_RESPONSE_MAX_ITEMS));
|
|
174
|
+
const start = budgetedStart(items, items.length, maxItems, window.byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
|
|
175
|
+
return start === 0 ? wire : { ...wire, items: items.slice(start), transcriptHasOlder: true };
|
|
113
176
|
}
|
|
114
177
|
|
|
115
178
|
/** A restored runtime transcript that starts after message 0 may have older
|
|
@@ -128,11 +191,68 @@ function runtimeArchiveRange(state) {
|
|
|
128
191
|
return Number.isInteger(after) && Number.isInteger(before) && before - 1 > after ? { after, before } : null;
|
|
129
192
|
}
|
|
130
193
|
|
|
194
|
+
/** The rows the runtime spilled out of memory above its first row
|
|
195
|
+
* (`transcriptSpilled`: `{ rows, epoch }`), published with the rows
|
|
196
|
+
* themselves and independent of the TUI's reading cursor; null when none. */
|
|
197
|
+
function spilledOf(state) {
|
|
198
|
+
const spilled = spillStateOf(state);
|
|
199
|
+
return spilled && spilled.rows > 0 ? spilled : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The published spill state, zero rows included (null: none published). */
|
|
203
|
+
function spillStateOf(state) {
|
|
204
|
+
const spilled = state?.transcriptSpilled;
|
|
205
|
+
const rows = Number(spilled?.rows);
|
|
206
|
+
const epoch = Number(spilled?.epoch);
|
|
207
|
+
return Number.isSafeInteger(rows) && rows >= 0 && Number.isSafeInteger(epoch) ? { rows, epoch } : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const sameSpill = (left, right) => left?.rows === right?.rows && left?.epoch === right?.epoch;
|
|
211
|
+
|
|
212
|
+
/** Whether spilled rows sit above the runtime's first row, readable through
|
|
213
|
+
* its spill accessor. A loaded head answers for itself. */
|
|
214
|
+
function spilledAbove(entry, state, view) {
|
|
215
|
+
return !view.head.length && spilledOf(state) !== null && typeof entry.runtime?.readSpilledTranscript === 'function';
|
|
216
|
+
}
|
|
217
|
+
|
|
131
218
|
function resetLoadedHead(view) {
|
|
132
219
|
view.head = [];
|
|
133
220
|
view.headOlder = null;
|
|
134
221
|
view.headSource = null;
|
|
135
222
|
view.archiveHeld = 0;
|
|
223
|
+
// Absolute index of the oldest spilled row the head holds (null: none).
|
|
224
|
+
view.spillFrom = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Older rows a spilled head leaves above itself: more spilled rows, or the
|
|
228
|
+
* source above the first spilled row. */
|
|
229
|
+
function spilledHeadOlder(view, archive) {
|
|
230
|
+
return view.spillFrom > 0 || (view.head.length > 0 && headSourceFor(view.head[0], archive) !== null);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The runtime spilled its oldest rows while the window showed them (its
|
|
234
|
+
* anchor among them): those rows join the head exactly as spilled, so the
|
|
235
|
+
* window keeps its rows and its place. False when the spill does not
|
|
236
|
+
* continue the rows this window last saw. */
|
|
237
|
+
function followSpilledAnchor(entry, view, wire) {
|
|
238
|
+
const spilled = spilledOf(wire);
|
|
239
|
+
const before = view.spilled;
|
|
240
|
+
const read = entry.runtime?.readSpilledTranscript;
|
|
241
|
+
if (!spilled || typeof read !== 'function' || view.start === null) return false;
|
|
242
|
+
// Rows spilled when the window last looked: none, or a count in this epoch.
|
|
243
|
+
if (before && before.epoch !== spilled.epoch) return false;
|
|
244
|
+
const from = (before?.rows ?? 0) + view.start;
|
|
245
|
+
if (from >= spilled.rows) return false;
|
|
246
|
+
const page = read({ epoch: spilled.epoch, end: spilled.rows, limit: spilled.rows - from });
|
|
247
|
+
if (!page || page.broken || page.start !== from || page.items[0]?.id !== view.anchorId) return false;
|
|
248
|
+
view.head = view.head.concat(sanitizeForWire(page.items));
|
|
249
|
+
if (view.headSource !== 'stored' && view.headSource !== 'archive') {
|
|
250
|
+
view.spillFrom ??= from;
|
|
251
|
+
view.headSource = 'spill';
|
|
252
|
+
view.headOlder = spilledHeadOlder(view, runtimeArchiveRange(wire));
|
|
253
|
+
}
|
|
254
|
+
view.start = 0;
|
|
255
|
+
return true;
|
|
136
256
|
}
|
|
137
257
|
|
|
138
258
|
/** Fold a read/subscribe request into a live entry's window. */
|
|
@@ -145,9 +265,11 @@ export function requestLiveTranscriptWindow(entry, request) {
|
|
|
145
265
|
if (view?.full) return;
|
|
146
266
|
if (!view) {
|
|
147
267
|
entry.transcriptView = {
|
|
148
|
-
|
|
268
|
+
// Its first window goes out whole: for a paged request,
|
|
269
|
+
// TRANSCRIPT_RESPONSE_MAX_ITEMS at most.
|
|
270
|
+
limit: request.paged ? Math.min(request.limit, TRANSCRIPT_RESPONSE_MAX_ITEMS) : request.limit,
|
|
149
271
|
byteBudget: request.byteBudget,
|
|
150
|
-
pageBase: request.pageBase,
|
|
272
|
+
pageBase: request.paged ? 0 : request.pageBase || 0,
|
|
151
273
|
start: null,
|
|
152
274
|
anchorId: undefined,
|
|
153
275
|
grow: false,
|
|
@@ -156,16 +278,29 @@ export function requestLiveTranscriptWindow(entry, request) {
|
|
|
156
278
|
paged: false,
|
|
157
279
|
head: [],
|
|
158
280
|
headOlder: null,
|
|
159
|
-
// Where the loaded head came from: '
|
|
160
|
-
//
|
|
281
|
+
// Where the loaded head came from: 'spill' (the rows the runtime spilled
|
|
282
|
+
// out of memory, exactly), 'stored' (durable history aligned by restore
|
|
283
|
+
// ids) or 'archive' (compaction-archive pages).
|
|
161
284
|
headSource: null,
|
|
162
285
|
archiveHeld: 0,
|
|
286
|
+
spillFrom: null,
|
|
287
|
+
// The spilled rows (`{ rows, epoch }`) when the window last looked.
|
|
288
|
+
spilled: null,
|
|
289
|
+
// Rows the window last showed.
|
|
290
|
+
size: 0,
|
|
163
291
|
cache: null,
|
|
164
292
|
};
|
|
165
293
|
return;
|
|
166
294
|
}
|
|
167
|
-
if (request.limit > view.limit) {
|
|
168
|
-
|
|
295
|
+
if (request.limit > view.limit && view.limit < TRANSCRIPT_WINDOW_MAX_ITEMS) {
|
|
296
|
+
// One paged request reveals at most TRANSCRIPT_RESPONSE_MAX_ITEMS older
|
|
297
|
+
// rows; any other names at most TRANSCRIPT_WINDOW_MAX_ITEMS. The shared
|
|
298
|
+
// window never grows past TRANSCRIPT_WINDOW_MAX_ITEMS: deeper history is
|
|
299
|
+
// a historical-position read (readTranscriptHistory), which no view shares.
|
|
300
|
+
view.limit = Math.min(
|
|
301
|
+
TRANSCRIPT_WINDOW_MAX_ITEMS,
|
|
302
|
+
request.paged ? Math.min(request.limit, view.size + TRANSCRIPT_RESPONSE_MAX_ITEMS) : request.limit
|
|
303
|
+
);
|
|
169
304
|
// The page's own budget bounds the rows it reveals.
|
|
170
305
|
view.grow = { byteBudget: request.byteBudget };
|
|
171
306
|
view.paged = true;
|
|
@@ -188,6 +323,36 @@ export function inheritTranscriptWindow(entry, view) {
|
|
|
188
323
|
);
|
|
189
324
|
}
|
|
190
325
|
|
|
326
|
+
/** The most rows the answer to `window` (requestedTranscriptWindow) carries
|
|
327
|
+
* whole, and its byte bound: a paged request's TRANSCRIPT_RESPONSE_MAX_ITEMS
|
|
328
|
+
* within its budget, any other's TRANSCRIPT_WINDOW_MAX_ITEMS. */
|
|
329
|
+
export function transcriptAnswerBounds(window) {
|
|
330
|
+
return window?.paged
|
|
331
|
+
? { maxItems: TRANSCRIPT_RESPONSE_MAX_ITEMS, byteBudget: window.byteBudget }
|
|
332
|
+
: { maxItems: TRANSCRIPT_WINDOW_MAX_ITEMS, byteBudget: null };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Whether `snapshot`, the entry's window about to go out whole, exceeds
|
|
336
|
+
* `bounds` (transcriptAnswerBounds). A legacy whole view has no window and
|
|
337
|
+
* never does. */
|
|
338
|
+
export function transcriptWindowOversized(entry, snapshot, bounds) {
|
|
339
|
+
const view = entry.transcriptView;
|
|
340
|
+
const items = snapshot?.items;
|
|
341
|
+
if (!view || view.full || !Array.isArray(items)) return false;
|
|
342
|
+
return !withinAnswerBounds(items, bounds.byteBudget, bounds.maxItems);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The window restarts from a fresh tail of at most `maxItems` rows at its
|
|
346
|
+
* next snapshot, as a first window: its readers page older rows again. */
|
|
347
|
+
export function restartTranscriptWindow(entry, maxItems) {
|
|
348
|
+
const view = entry.transcriptView;
|
|
349
|
+
if (!view || view.full) return;
|
|
350
|
+
view.start = null;
|
|
351
|
+
view.paged = true;
|
|
352
|
+
view.limit = Math.min(view.limit, maxItems);
|
|
353
|
+
view.pageBase = 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
191
356
|
/** The entry's wire snapshot seen through its window. Identity-stable for an
|
|
192
357
|
* unchanged source and window, so revision steps still detect "unchanged". */
|
|
193
358
|
export function windowTranscriptSnapshot(entry, wire) {
|
|
@@ -195,17 +360,34 @@ export function windowTranscriptSnapshot(entry, wire) {
|
|
|
195
360
|
if (!view || view.full || !wire || !Array.isArray(wire.items)) return wire;
|
|
196
361
|
const items = wire.items;
|
|
197
362
|
let start = view.start;
|
|
363
|
+
const spill = spillStateOf(wire);
|
|
364
|
+
// The spill store was reset or restored (another epoch, or no spill state):
|
|
365
|
+
// spilled and older rows the window loaded name another page list, so it
|
|
366
|
+
// restarts from the tail whether or not its anchor is still live.
|
|
367
|
+
if (view.spilled && spill?.epoch !== view.spilled.epoch) start = null;
|
|
198
368
|
if (start !== null && view.anchorId !== undefined && items[start]?.id !== view.anchorId) {
|
|
199
|
-
// The runtime rewrote its transcript before the anchor (compaction,
|
|
200
|
-
// rewind, clear): follow the anchor, or restart from the tail.
|
|
201
369
|
const moved = items.findIndex((item) => item?.id === view.anchorId);
|
|
202
|
-
|
|
203
|
-
|
|
370
|
+
if (moved < 0 && followSpilledAnchor(entry, view, wire)) {
|
|
371
|
+
// The runtime spilled the rows the window starts with: they are head now.
|
|
372
|
+
start = 0;
|
|
373
|
+
} else {
|
|
374
|
+
// The runtime rewrote its transcript before the anchor (compaction,
|
|
375
|
+
// rewind, clear): follow the anchor, or restart from the tail.
|
|
376
|
+
start = moved < 0 ? null : moved;
|
|
377
|
+
resetLoadedHead(view);
|
|
378
|
+
}
|
|
204
379
|
}
|
|
205
380
|
if (start !== null && start > items.length) start = null;
|
|
206
381
|
if (start === null) {
|
|
207
382
|
start = tailWindowStart(items, view);
|
|
208
383
|
resetLoadedHead(view);
|
|
384
|
+
if (view.paged) {
|
|
385
|
+
// A paged window restarted from a fresh tail is a first window again:
|
|
386
|
+
// its limit is what it shows, so the reader's next page grows it.
|
|
387
|
+
view.paged = false;
|
|
388
|
+
view.grow = false;
|
|
389
|
+
view.limit = items.length - start;
|
|
390
|
+
}
|
|
209
391
|
}
|
|
210
392
|
// The sticky start lets appends travel as suffix patches, so an unpaged
|
|
211
393
|
// tail grows past its byte budget while views stay attached (a worker
|
|
@@ -223,10 +405,19 @@ export function windowTranscriptSnapshot(entry, wire) {
|
|
|
223
405
|
start = budgetedStart(items, start, view.limit - held, view.grow.byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
|
|
224
406
|
view.grow = false;
|
|
225
407
|
}
|
|
408
|
+
if (!view.private && view.head.length + items.length - start > TRANSCRIPT_WINDOW_MAX_ITEMS) {
|
|
409
|
+
// Appends under a sticky start outgrew the shared window: its oldest rows
|
|
410
|
+
// go (a page's worth below the bound, so this does not recur per append).
|
|
411
|
+
resetLoadedHead(view);
|
|
412
|
+
start = Math.max(start, items.length - (TRANSCRIPT_WINDOW_MAX_ITEMS - TRANSCRIPT_RESPONSE_MAX_ITEMS));
|
|
413
|
+
}
|
|
226
414
|
view.start = start;
|
|
227
415
|
view.anchorId = items[start]?.id;
|
|
416
|
+
view.spilled = spill;
|
|
228
417
|
const older =
|
|
229
|
-
start > 0 ||
|
|
418
|
+
start > 0 ||
|
|
419
|
+
(view.headOlder ??
|
|
420
|
+
headSourceFor(view.head[0] ?? items[0], runtimeArchiveRange(wire), spilledAbove(entry, wire, view)) !== null);
|
|
230
421
|
const cache = view.cache;
|
|
231
422
|
if (cache && cache.wire === wire && cache.start === start && cache.head === view.head && cache.older === older) {
|
|
232
423
|
return cache.value;
|
|
@@ -236,14 +427,17 @@ export function windowTranscriptSnapshot(entry, wire) {
|
|
|
236
427
|
else if (start) windowed = items.slice(start);
|
|
237
428
|
const value = { ...wire, items: windowed, transcriptHasOlder: older };
|
|
238
429
|
view.cache = { wire, start, head: view.head, older, value };
|
|
430
|
+
view.size = windowed.length;
|
|
239
431
|
return value;
|
|
240
432
|
}
|
|
241
433
|
|
|
242
|
-
/** Where older rows above `first` come from:
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
|
|
434
|
+
/** Where older rows above `first` come from: the rows the runtime spilled out
|
|
435
|
+
* of memory above it (`spilled`), then durable history aligned by restore
|
|
436
|
+
* ids (which continues into the compaction archive), or — once the runtime's
|
|
437
|
+
* rows are not restored ones (a live compaction archived them, or nothing was
|
|
438
|
+
* restored) — compaction-archive pages alone. */
|
|
439
|
+
function headSourceFor(first, archive, spilled = false) {
|
|
440
|
+
if (spilled) return 'spill';
|
|
247
441
|
const restoredId = RESTORED_ITEM_ID.test(String(first?.id ?? ''));
|
|
248
442
|
if (restoredId && first?.archived !== true) return restoredAfterFirstMessage(first) || archive ? 'stored' : null;
|
|
249
443
|
if (archive) return 'archive';
|
|
@@ -324,11 +518,12 @@ function verifiedIdentityIndex(history, first, live) {
|
|
|
324
518
|
}
|
|
325
519
|
|
|
326
520
|
/** Load history OLDER than the runtime's first item when a grown window
|
|
327
|
-
* reaches past what the runtime holds (
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
521
|
+
* reaches past what the runtime holds (the runtime spilled its oldest rows
|
|
522
|
+
* out of memory; a resumed runtime restores only a bounded tail; a compaction
|
|
523
|
+
* moved earlier rows to the display-only archive). Spilled rows come first,
|
|
524
|
+
* exactly as spilled; then durable items align by their deterministic
|
|
525
|
+
* restore ids; archive pages continue below the archived rows already held.
|
|
526
|
+
* Returns whether durable history was read. */
|
|
332
527
|
export async function loadLiveTranscriptHead(entry, sessionId, readStoredSession) {
|
|
333
528
|
const view = entry.transcriptView;
|
|
334
529
|
if (!view || view.full || !view.paged || typeof readStoredSession !== 'function' || view.headOlder === false) {
|
|
@@ -340,12 +535,19 @@ export async function loadLiveTranscriptHead(entry, sessionId, readStoredSession
|
|
|
340
535
|
const state = entry.runtime?.getState?.();
|
|
341
536
|
const runtimeItems = Array.isArray(state?.items) ? state.items : [];
|
|
342
537
|
const missing = view.limit - runtimeItems.length - view.head.length;
|
|
538
|
+
// The rows were spilled since the window last looked: its next snapshot
|
|
539
|
+
// moves them into the head first, so spilled indices stay its own.
|
|
540
|
+
if (!sameSpill(spillStateOf(state), view.spilled)) return false;
|
|
541
|
+
const spilled = spilledOf(state);
|
|
343
542
|
const first = view.head[0] ?? runtimeItems[0];
|
|
344
543
|
const archive = runtimeArchiveRange(state);
|
|
345
|
-
const
|
|
544
|
+
const spilling = view.headSource === 'spill' ? view.spillFrom > 0 : spilledAbove(entry, state, view);
|
|
545
|
+
const source = spilling ? 'spill' : (view.headSource !== 'spill' && view.headSource) || headSourceFor(first, archive);
|
|
346
546
|
if (missing <= 0 || !source) return false;
|
|
347
547
|
const generation = compactionGeneration(runtimeItems);
|
|
348
|
-
|
|
548
|
+
// Spilled rows are the runtime's own, joined exactly whatever compacted
|
|
549
|
+
// since; a durable head is valid only for its generation.
|
|
550
|
+
if (view.head.length && view.headSource !== 'spill' && view.headGeneration !== generation) {
|
|
349
551
|
// A compaction since the head was loaded: keep the rows as they are, and
|
|
350
552
|
// never join older rows across the generation.
|
|
351
553
|
view.headOlder = false;
|
|
@@ -357,9 +559,26 @@ export async function loadLiveTranscriptHead(entry, sessionId, readStoredSession
|
|
|
357
559
|
entry.transcriptView === view &&
|
|
358
560
|
(view.head[0] ?? currentItems()?.[0]) === first &&
|
|
359
561
|
compactionGeneration(Array.isArray(currentItems()) ? currentItems() : []) === generation;
|
|
562
|
+
if (source === 'spill') {
|
|
563
|
+
// Synchronous: the rows read are exactly the ones above `first` now.
|
|
564
|
+
const end = view.spillFrom ?? spilled.rows;
|
|
565
|
+
const page = entry.runtime.readSpilledTranscript({ epoch: spilled.epoch, end, limit: missing });
|
|
566
|
+
if (!page) return false;
|
|
567
|
+
const rows = page.items;
|
|
568
|
+
const from = budgetedStart(rows, rows.length, missing, pageBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
|
|
569
|
+
view.head = sanitizeForWire(rows.slice(from)).concat(view.head);
|
|
570
|
+
view.spillFrom = end - (rows.length - from);
|
|
571
|
+
view.headSource = 'spill';
|
|
572
|
+
// An unreadable page ends the history there: nothing above it joins
|
|
573
|
+
// without a gap. Past the spilled rows, the first of them names the next
|
|
574
|
+
// source.
|
|
575
|
+
view.headOlder = page.broken && from === 0 ? false : spilledHeadOlder(view, archive);
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
360
578
|
if (source === 'archive') {
|
|
361
579
|
const page = await readStoredSession(sessionId, {
|
|
362
580
|
compactionArchive: { ...archive, skip: view.archiveHeld || 0, limit: missing },
|
|
581
|
+
paging: true,
|
|
363
582
|
});
|
|
364
583
|
if (!stillCurrent()) return true;
|
|
365
584
|
const rows = Array.isArray(page?.items) ? page.items : [];
|
|
@@ -371,10 +590,15 @@ export async function loadLiveTranscriptHead(entry, sessionId, readStoredSession
|
|
|
371
590
|
view.headOlder = from > 0 || page?.hasOlder === true;
|
|
372
591
|
return true;
|
|
373
592
|
}
|
|
374
|
-
const stored = await readStoredSession(sessionId, {
|
|
593
|
+
const stored = await readStoredSession(sessionId, {
|
|
594
|
+
transcriptItemLimit: view.limit + runtimeItems.length,
|
|
595
|
+
paging: true,
|
|
596
|
+
});
|
|
375
597
|
if (!stillCurrent()) return true;
|
|
376
598
|
const history = Array.isArray(stored?.items) ? stored.items : [];
|
|
377
|
-
|
|
599
|
+
// Only a head read from this durable history aligns by its ids; spilled
|
|
600
|
+
// rows are live rows like the runtime's own.
|
|
601
|
+
const overlap = alignedHistoryIndex(history, first, view.head.concat(runtimeItems), view.headSource === 'stored');
|
|
378
602
|
if (overlap < 0) {
|
|
379
603
|
view.headOlder = false;
|
|
380
604
|
return true;
|
|
@@ -387,6 +611,402 @@ export async function loadLiveTranscriptHead(entry, sessionId, readStoredSession
|
|
|
387
611
|
return true;
|
|
388
612
|
}
|
|
389
613
|
|
|
614
|
+
/** The newest `requested` rows of a live session through a private window
|
|
615
|
+
* (never the entry's own, which its views share): spilled rows, durable
|
|
616
|
+
* history and archive load above the runtime's rows as for a view. */
|
|
617
|
+
async function isolatedLiveRows(entry, sessionId, readStoredSession, requested) {
|
|
618
|
+
const state = entry.runtime.getState();
|
|
619
|
+
const isolated = {
|
|
620
|
+
runtime: { getState: () => state, readSpilledTranscript: entry.runtime.readSpilledTranscript },
|
|
621
|
+
};
|
|
622
|
+
requestLiveTranscriptWindow(isolated, { limit: requested, byteBudget: null, pageBase: 0 });
|
|
623
|
+
const view = isolated.transcriptView;
|
|
624
|
+
// Not shared: the shared window's row cap does not apply.
|
|
625
|
+
view.private = true;
|
|
626
|
+
windowTranscriptSnapshot(isolated, state);
|
|
627
|
+
view.limit = requested;
|
|
628
|
+
view.paged = true;
|
|
629
|
+
view.grow = { byteBudget: null };
|
|
630
|
+
let snapshot = windowTranscriptSnapshot(isolated, state);
|
|
631
|
+
// One load reads one source (spilled rows, durable history, archive): keep
|
|
632
|
+
// loading until the rows are covered, history ends, or a load adds nothing.
|
|
633
|
+
while (snapshot.items.length < requested && snapshot.transcriptHasOlder) {
|
|
634
|
+
const held = snapshot.items.length;
|
|
635
|
+
await loadLiveTranscriptHead(isolated, sessionId, readStoredSession);
|
|
636
|
+
snapshot = windowTranscriptSnapshot(isolated, state);
|
|
637
|
+
if (snapshot.items.length <= held) break;
|
|
638
|
+
}
|
|
639
|
+
privateViews.set(snapshot, view);
|
|
640
|
+
return snapshot;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** The private window each isolatedLiveRows snapshot was read through. */
|
|
644
|
+
const privateViews = new WeakMap();
|
|
645
|
+
|
|
646
|
+
/** Positions (see HISTORY_POSITION) of rows `[from, to)` of a private
|
|
647
|
+
* window's snapshot: its loaded head is archive rows (when it reached them)
|
|
648
|
+
* then spilled rows from `spillFrom`; other rows need none. */
|
|
649
|
+
function privateRowPositions(snapshot, state, from, to) {
|
|
650
|
+
const view = privateViews.get(snapshot);
|
|
651
|
+
const spilled = spilledOf(state);
|
|
652
|
+
const archive = runtimeArchiveRange(state);
|
|
653
|
+
const head = view?.head.length ?? 0;
|
|
654
|
+
const spillRows = view && view.spillFrom !== null && spilled ? spilled.rows - view.spillFrom : 0;
|
|
655
|
+
const archiveRows = view?.headSource === 'archive' ? head - spillRows : 0;
|
|
656
|
+
const positions = [];
|
|
657
|
+
for (let index = from; index < to; index += 1) {
|
|
658
|
+
if (index < archiveRows && archive) positions.push(archivePosition(archive, archiveRows - 1 - index));
|
|
659
|
+
else if (index >= head - spillRows && index < head && spilled) {
|
|
660
|
+
positions.push(spillPosition(spilled.epoch, view.spillFrom + index - (head - spillRows)));
|
|
661
|
+
} else positions.push(null);
|
|
662
|
+
}
|
|
663
|
+
return positions;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** The most rows one historical-position read carries. */
|
|
667
|
+
export const TRANSCRIPT_HISTORY_MAX_ITEMS = TRANSCRIPT_RESPONSE_MAX_ITEMS;
|
|
668
|
+
|
|
669
|
+
/** A historical-position read (`transcriptHistory`: `{ before | after: row
|
|
670
|
+
* id, limit, byteBudget? }`), validated; null when the params name none. */
|
|
671
|
+
export function requestedTranscriptHistory(params) {
|
|
672
|
+
const request = params?.transcriptHistory;
|
|
673
|
+
if (request == null) return null;
|
|
674
|
+
const limit = Number(request?.limit);
|
|
675
|
+
const older = request?.before != null;
|
|
676
|
+
const id = older ? request.before : request?.after;
|
|
677
|
+
const budget = Number(request?.byteBudget);
|
|
678
|
+
if (
|
|
679
|
+
(typeof id !== 'string' && typeof id !== 'number') ||
|
|
680
|
+
(older && request.after != null) ||
|
|
681
|
+
!Number.isInteger(limit) ||
|
|
682
|
+
limit < 1 ||
|
|
683
|
+
limit > TRANSCRIPT_HISTORY_MAX_ITEMS
|
|
684
|
+
) {
|
|
685
|
+
throw new TypeError('invalid transcript history request');
|
|
686
|
+
}
|
|
687
|
+
const position = request.position;
|
|
688
|
+
if (position != null && (typeof position !== 'string' || !HISTORY_POSITION.test(position))) {
|
|
689
|
+
throw new TypeError('invalid transcript history request');
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
...(older ? { before: id } : { after: id }),
|
|
693
|
+
limit,
|
|
694
|
+
byteBudget: Number.isSafeInteger(budget) && budget > 0 ? budget : null,
|
|
695
|
+
...(position != null ? { position } : {}),
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const historyAnswer = (sessionId, items, hasOlder, hasNewer, positions = null) => ({
|
|
700
|
+
sessionId,
|
|
701
|
+
items: sanitizeForWire(items),
|
|
702
|
+
hasOlder: hasOlder === true,
|
|
703
|
+
hasNewer: hasNewer === true,
|
|
704
|
+
...(positions?.some((position) => position !== null) ? { positions } : {}),
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
/** Where a row of a live session's history sits in a store addressed by
|
|
708
|
+
* position — `s:<epoch>:<index>`: a spilled row (absolute index in that spill
|
|
709
|
+
* epoch); `a:<after>:<before>:<skip>`: a compaction-archive row (`skip` rows
|
|
710
|
+
* newer than it in that archive range). An answer names its rows' positions
|
|
711
|
+
* (`positions`) and a request may name its boundary row's (`position`): the
|
|
712
|
+
* row is then read where it is — one page, verified by id — never searched
|
|
713
|
+
* for. Rows of the runtime and of durable history need none (found by id). */
|
|
714
|
+
const HISTORY_POSITION = /^(?:s:\d{1,15}:\d{1,15}|a:\d{1,15}:\d{1,15}:\d{1,15})$/;
|
|
715
|
+
|
|
716
|
+
function parseHistoryPosition(position) {
|
|
717
|
+
if (typeof position !== 'string' || !HISTORY_POSITION.test(position)) return null;
|
|
718
|
+
const [kind, ...numbers] = position.split(':');
|
|
719
|
+
const values = numbers.map(Number);
|
|
720
|
+
return kind === 's'
|
|
721
|
+
? { kind, epoch: values[0], index: values[1] }
|
|
722
|
+
: { kind, after: values[0], before: values[1], skip: values[2] };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const spillPosition = (epoch, index) => `s:${epoch}:${index}`;
|
|
726
|
+
const archivePosition = (range, skip) => `a:${range.after}:${range.before}:${skip}`;
|
|
727
|
+
|
|
728
|
+
/** Older (`before`) or newer rows than one positioned row, read where they
|
|
729
|
+
* are: spilled rows by absolute index, archive rows by skip, past either end
|
|
730
|
+
* into the next source (spill → archive or durable history above it; archive
|
|
731
|
+
* → spill or runtime below it). Null when the position no longer names that
|
|
732
|
+
* row (another epoch or range, or another row there): the caller looks up
|
|
733
|
+
* the row instead. Every read is one bounded page. */
|
|
734
|
+
async function readAtHistoryPosition(entry, state, sessionId, readStoredSession, request, older, id, position) {
|
|
735
|
+
const items = Array.isArray(state?.items) ? state.items : [];
|
|
736
|
+
const spilled = spilledOf(state);
|
|
737
|
+
const readSpill =
|
|
738
|
+
typeof entry.runtime?.readSpilledTranscript === 'function'
|
|
739
|
+
? (range) => entry.runtime.readSpilledTranscript(range)
|
|
740
|
+
: null;
|
|
741
|
+
const archive = runtimeArchiveRange(state);
|
|
742
|
+
const readArchive = async (skip, limit) => {
|
|
743
|
+
if (!archive || limit <= 0 || typeof readStoredSession !== 'function') return null;
|
|
744
|
+
const page = await readStoredSession(sessionId, { compactionArchive: { ...archive, skip, limit }, paging: true });
|
|
745
|
+
return Array.isArray(page?.items) ? page : null;
|
|
746
|
+
};
|
|
747
|
+
const { limit, byteBudget } = request;
|
|
748
|
+
// Rows below the archive or the spill: the spill's first rows, else the runtime's.
|
|
749
|
+
const newerFromSpillStart = () => {
|
|
750
|
+
if (spilled && readSpill) {
|
|
751
|
+
const page = readSpill({ epoch: spilled.epoch, end: Math.min(spilled.rows, limit), limit });
|
|
752
|
+
// An unreadable spill page is a gap: never skipped to the runtime's rows.
|
|
753
|
+
if (!page || page.broken || page.start !== 0) return historyReset(sessionId);
|
|
754
|
+
const end = budgetedEnd(page.items, 0, limit, byteBudget);
|
|
755
|
+
const rows = page.items.slice(0, end);
|
|
756
|
+
return historyAnswer(
|
|
757
|
+
sessionId,
|
|
758
|
+
rows,
|
|
759
|
+
true,
|
|
760
|
+
true,
|
|
761
|
+
rows.map((_, i) => spillPosition(spilled.epoch, i))
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
const end = budgetedEnd(items, 0, limit, byteBudget);
|
|
765
|
+
return historyAnswer(sessionId, items.slice(0, end), true, end < items.length);
|
|
766
|
+
};
|
|
767
|
+
// Rows above the spill's first row (or the runtime's, when nothing spilled).
|
|
768
|
+
const olderThanSpillStart = async (first) => {
|
|
769
|
+
const source = headSourceFor(first, archive);
|
|
770
|
+
if (source === 'archive') return olderFromArchive(-1);
|
|
771
|
+
if (source === 'stored' && typeof readStoredSession === 'function' && first?.id != null) {
|
|
772
|
+
const page = await readStoredSession(sessionId, {
|
|
773
|
+
transcriptCursor: { before: first.id, maxRows: limit, byteBudget },
|
|
774
|
+
paging: true,
|
|
775
|
+
});
|
|
776
|
+
if (page && !page.cursorMissing && typeof page.transcriptHasNewer === 'boolean') {
|
|
777
|
+
return historyAnswer(sessionId, page.items, page.transcriptHasOlder, true);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return historyAnswer(sessionId, [], false, true);
|
|
781
|
+
};
|
|
782
|
+
// Archive rows older than the one `skip` rows from its newest (-1: from the newest).
|
|
783
|
+
const olderFromArchive = async (skip) => {
|
|
784
|
+
const page = await readArchive(skip + 1, limit);
|
|
785
|
+
if (!page) return historyAnswer(sessionId, [], false, true);
|
|
786
|
+
const start = budgetedStart(page.items, page.items.length, limit, byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
|
|
787
|
+
const rows = page.items.slice(start);
|
|
788
|
+
const positions = rows.map((_, i) => archivePosition(archive, skip + rows.length - i));
|
|
789
|
+
return historyAnswer(sessionId, rows, start > 0 || page.hasOlder === true, true, positions);
|
|
790
|
+
};
|
|
791
|
+
// Spilled rows ending just above index `end`.
|
|
792
|
+
const olderFromSpill = (end) => {
|
|
793
|
+
const page = readSpill({ epoch: spilled.epoch, end, limit });
|
|
794
|
+
if (!page || page.broken) return null;
|
|
795
|
+
const start = budgetedStart(page.items, page.items.length, limit, byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
|
|
796
|
+
const first = page.start + start;
|
|
797
|
+
const rows = page.items.slice(start);
|
|
798
|
+
const above = first > 0 || headSourceFor(rows[0], archive) !== null;
|
|
799
|
+
return historyAnswer(
|
|
800
|
+
sessionId,
|
|
801
|
+
rows,
|
|
802
|
+
above,
|
|
803
|
+
true,
|
|
804
|
+
rows.map((_, i) => spillPosition(spilled.epoch, first + i))
|
|
805
|
+
);
|
|
806
|
+
};
|
|
807
|
+
if (position.kind === 'runtime-start') {
|
|
808
|
+
// Older than the runtime's first row: the spill's last rows, else what
|
|
809
|
+
// lies above the runtime.
|
|
810
|
+
if (spilled && readSpill) return olderFromSpill(spilled.rows);
|
|
811
|
+
return olderThanSpillStart(items[0]);
|
|
812
|
+
}
|
|
813
|
+
if (position.kind === 's') {
|
|
814
|
+
if (!spilled || !readSpill || position.epoch !== spilled.epoch || position.index >= spilled.rows) return null;
|
|
815
|
+
const at = readSpill({ epoch: spilled.epoch, end: position.index + 1, limit: 1 });
|
|
816
|
+
if (!at || at.broken || at.items[0]?.id !== id) return null;
|
|
817
|
+
if (older) return position.index === 0 ? olderThanSpillStart(at.items[0]) : olderFromSpill(position.index);
|
|
818
|
+
const from = position.index + 1;
|
|
819
|
+
if (from >= spilled.rows) {
|
|
820
|
+
const end = budgetedEnd(items, 0, limit, byteBudget);
|
|
821
|
+
return historyAnswer(sessionId, items.slice(0, end), true, end < items.length);
|
|
822
|
+
}
|
|
823
|
+
const page = readSpill({ epoch: spilled.epoch, end: Math.min(spilled.rows, from + limit), limit });
|
|
824
|
+
if (!page || page.broken || page.start !== from) return null;
|
|
825
|
+
const end = budgetedEnd(page.items, 0, limit, byteBudget);
|
|
826
|
+
const rows = page.items.slice(0, end);
|
|
827
|
+
return historyAnswer(
|
|
828
|
+
sessionId,
|
|
829
|
+
rows,
|
|
830
|
+
true,
|
|
831
|
+
true,
|
|
832
|
+
rows.map((_, i) => spillPosition(spilled.epoch, from + i))
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (!archive || position.after !== archive.after || position.before !== archive.before) return null;
|
|
836
|
+
const at = await readArchive(position.skip, 1);
|
|
837
|
+
if (!at || at.items.at(-1)?.id !== id) return null;
|
|
838
|
+
if (older) return olderFromArchive(position.skip);
|
|
839
|
+
if (position.skip === 0) return newerFromSpillStart();
|
|
840
|
+
const take = Math.min(limit, position.skip);
|
|
841
|
+
const page = await readArchive(position.skip - take, take);
|
|
842
|
+
if (!page) return null;
|
|
843
|
+
const end = budgetedEnd(page.items, 0, limit, byteBudget);
|
|
844
|
+
const rows = page.items.slice(0, end);
|
|
845
|
+
return historyAnswer(
|
|
846
|
+
sessionId,
|
|
847
|
+
rows,
|
|
848
|
+
true,
|
|
849
|
+
true,
|
|
850
|
+
rows.map((_, i) => archivePosition(archive, position.skip - 1 - i))
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
const historyReset = (sessionId) => ({ sessionId, items: [], hasOlder: false, hasNewer: false, reset: true });
|
|
854
|
+
|
|
855
|
+
/** Rows next to one row of the session's whole history — the rows directly
|
|
856
|
+
* older than `before`, or newer than `after` — at most `limit` within
|
|
857
|
+
* `byteBudget` (never fewer than TRANSCRIPT_WINDOW_MIN_ITEMS), with whether
|
|
858
|
+
* more exist each way. Stateless and read-only: no view's window, baseline or
|
|
859
|
+
* revision moves, so a reader deep in history never grows what live views
|
|
860
|
+
* share. Rows are found by their stable ids (append-only history keeps
|
|
861
|
+
* them); a row no longer there answers `reset` and the reader returns to the
|
|
862
|
+
* live tail. */
|
|
863
|
+
export async function readTranscriptHistory(entry, sessionId, readStoredSession, request) {
|
|
864
|
+
const older = request.before != null;
|
|
865
|
+
const id = older ? request.before : request.after;
|
|
866
|
+
const stored = async (rowId, before) => {
|
|
867
|
+
if (typeof readStoredSession !== 'function') return null;
|
|
868
|
+
const page = await readStoredSession(sessionId, {
|
|
869
|
+
transcriptCursor: {
|
|
870
|
+
...(before ? { before: rowId } : { after: rowId }),
|
|
871
|
+
maxRows: request.limit,
|
|
872
|
+
byteBudget: request.byteBudget,
|
|
873
|
+
},
|
|
874
|
+
paging: true,
|
|
875
|
+
});
|
|
876
|
+
// A reader without historical positions answers no cursor fields.
|
|
877
|
+
if (!page || page.cursorMissing || typeof page.transcriptHasNewer !== 'boolean') return null;
|
|
878
|
+
return page;
|
|
879
|
+
};
|
|
880
|
+
if (!entry) {
|
|
881
|
+
const page = await stored(id, older);
|
|
882
|
+
return page
|
|
883
|
+
? historyAnswer(sessionId, page.items, page.transcriptHasOlder, page.transcriptHasNewer)
|
|
884
|
+
: historyReset(sessionId);
|
|
885
|
+
}
|
|
886
|
+
const state = entry.runtime.getState();
|
|
887
|
+
const items = Array.isArray(state?.items) ? state.items : [];
|
|
888
|
+
const at = items.findIndex((item) => item?.id === id);
|
|
889
|
+
const spilled = spilledOf(state) !== null && typeof entry.runtime?.readSpilledTranscript === 'function';
|
|
890
|
+
const olderSource = headSourceFor(items[0], runtimeArchiveRange(state), spilled);
|
|
891
|
+
if (older && at > 0) {
|
|
892
|
+
const start = budgetedStart(items, at, request.limit, request.byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
|
|
893
|
+
return historyAnswer(sessionId, items.slice(start, at), start > 0 || olderSource !== null, true);
|
|
894
|
+
}
|
|
895
|
+
if (!older && at >= 0) {
|
|
896
|
+
const end = budgetedEnd(items, at + 1, request.limit, request.byteBudget);
|
|
897
|
+
return historyAnswer(sessionId, items.slice(at + 1, end), true, end < items.length);
|
|
898
|
+
}
|
|
899
|
+
// A row whose position the reader names (spilled or archived) is read
|
|
900
|
+
// where it is, and so is what lies above the runtime's first row.
|
|
901
|
+
const position = parseHistoryPosition(request.position);
|
|
902
|
+
if (position) {
|
|
903
|
+
const answer = await readAtHistoryPosition(
|
|
904
|
+
entry,
|
|
905
|
+
state,
|
|
906
|
+
sessionId,
|
|
907
|
+
readStoredSession,
|
|
908
|
+
request,
|
|
909
|
+
older,
|
|
910
|
+
id,
|
|
911
|
+
position
|
|
912
|
+
);
|
|
913
|
+
if (answer) return answer;
|
|
914
|
+
}
|
|
915
|
+
if (older && at === 0 && (olderSource === 'spill' || olderSource === 'archive')) {
|
|
916
|
+
const answer = await readAtHistoryPosition(entry, state, sessionId, readStoredSession, request, true, id, {
|
|
917
|
+
kind: 'runtime-start',
|
|
918
|
+
});
|
|
919
|
+
if (answer) return answer;
|
|
920
|
+
}
|
|
921
|
+
// Above the runtime's rows. Durable history aligns with restored rows by
|
|
922
|
+
// their ids while no compaction rewrote them: read it there directly.
|
|
923
|
+
if (olderSource === 'stored' && !items.some(isCompactionMark)) {
|
|
924
|
+
const page = await stored(older && at === 0 ? items[0].id : id, older);
|
|
925
|
+
if (!page) return historyReset(sessionId);
|
|
926
|
+
if (older) return historyAnswer(sessionId, page.items, page.transcriptHasOlder, true);
|
|
927
|
+
// Newer rows run into the runtime's own rows: continue there.
|
|
928
|
+
const joined = page.items.findIndex((item) => item?.id === items[0]?.id);
|
|
929
|
+
let rows = joined >= 0 ? page.items.slice(0, joined) : page.items;
|
|
930
|
+
if (rows.length === 0 && (joined >= 0 || !page.transcriptHasNewer)) {
|
|
931
|
+
rows = items.slice(0, budgetedEnd(items, 0, request.limit, request.byteBudget));
|
|
932
|
+
return historyAnswer(sessionId, rows, true, rows.length < items.length);
|
|
933
|
+
}
|
|
934
|
+
return historyAnswer(sessionId, rows, true, true);
|
|
935
|
+
}
|
|
936
|
+
// Spilled or archived rows (a live compaction): a private window loaded
|
|
937
|
+
// down to the row, deeper on each pass — but never more than
|
|
938
|
+
// TRANSCRIPT_HISTORY_LOOKUP_MAX_ITEMS rows above the runtime's own, nor on
|
|
939
|
+
// after a pass that loaded nothing new. Past that the row answers `reset`:
|
|
940
|
+
// no lookup here scales with the history's depth.
|
|
941
|
+
const bound = items.length + TRANSCRIPT_HISTORY_LOOKUP_MAX_ITEMS;
|
|
942
|
+
let requested = Math.min(bound, items.length + Math.max(request.limit * 2, 64));
|
|
943
|
+
let loaded = -1;
|
|
944
|
+
for (;;) {
|
|
945
|
+
const snapshot = await isolatedLiveRows(entry, sessionId, readStoredSession, requested);
|
|
946
|
+
const rows = snapshot.items;
|
|
947
|
+
const index = rows.findIndex((item) => item?.id === id);
|
|
948
|
+
// Found where a position names it: the page is read there.
|
|
949
|
+
const [own] = index >= 0 ? privateRowPositions(snapshot, state, index, index + 1) : [];
|
|
950
|
+
if (own) {
|
|
951
|
+
const answer = await readAtHistoryPosition(
|
|
952
|
+
entry,
|
|
953
|
+
state,
|
|
954
|
+
sessionId,
|
|
955
|
+
readStoredSession,
|
|
956
|
+
request,
|
|
957
|
+
older,
|
|
958
|
+
id,
|
|
959
|
+
parseHistoryPosition(own)
|
|
960
|
+
);
|
|
961
|
+
if (answer) return answer;
|
|
962
|
+
}
|
|
963
|
+
if (index >= 0 && (!older || index >= request.limit || !snapshot.transcriptHasOlder || requested >= bound)) {
|
|
964
|
+
if (older) {
|
|
965
|
+
const start = budgetedStart(rows, index, request.limit, request.byteBudget, TRANSCRIPT_WINDOW_MIN_ITEMS);
|
|
966
|
+
return historyAnswer(
|
|
967
|
+
sessionId,
|
|
968
|
+
rows.slice(start, index),
|
|
969
|
+
start > 0 || snapshot.transcriptHasOlder,
|
|
970
|
+
true,
|
|
971
|
+
privateRowPositions(snapshot, state, start, index)
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
const end = budgetedEnd(rows, index + 1, request.limit, request.byteBudget);
|
|
975
|
+
return historyAnswer(
|
|
976
|
+
sessionId,
|
|
977
|
+
rows.slice(index + 1, end),
|
|
978
|
+
true,
|
|
979
|
+
end < rows.length,
|
|
980
|
+
privateRowPositions(snapshot, state, index + 1, end)
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
if (!snapshot.transcriptHasOlder || rows.length < requested || rows.length <= loaded || requested >= bound) {
|
|
984
|
+
return historyReset(sessionId);
|
|
985
|
+
}
|
|
986
|
+
loaded = rows.length;
|
|
987
|
+
requested = Math.min(bound, requested * 2);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/** The most rows a live historical-position lookup loads above the runtime's
|
|
992
|
+
* own (spilled or archived ones), as the stored reader's own bound. */
|
|
993
|
+
const TRANSCRIPT_HISTORY_LOOKUP_MAX_ITEMS = TRANSCRIPT_WINDOW_MAX_ITEMS;
|
|
994
|
+
|
|
995
|
+
/** Last index (exclusive) of the rows kept from `start`: at most `maxItems`
|
|
996
|
+
* and, past TRANSCRIPT_WINDOW_MIN_ITEMS, within `byteBudget`. */
|
|
997
|
+
function budgetedEnd(items, start, maxItems, byteBudget) {
|
|
998
|
+
let end = start;
|
|
999
|
+
let bytes = 0;
|
|
1000
|
+
while (end < items.length && end - start < maxItems) {
|
|
1001
|
+
const next = itemBytes(items[end]);
|
|
1002
|
+
if (byteBudget && end - start >= Math.min(maxItems, TRANSCRIPT_WINDOW_MIN_ITEMS) && bytes + next > byteBudget)
|
|
1003
|
+
break;
|
|
1004
|
+
bytes += next;
|
|
1005
|
+
end += 1;
|
|
1006
|
+
}
|
|
1007
|
+
return end;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
390
1010
|
/** A one-shot range must not grow or rebudget an attached desktop's window. */
|
|
391
1011
|
export async function readTranscriptRange(entry, sessionId, readStoredSession, { offset, limit }) {
|
|
392
1012
|
if (
|
|
@@ -401,15 +1021,31 @@ export async function readTranscriptRange(entry, sessionId, readStoredSession, {
|
|
|
401
1021
|
const requested = offset + limit;
|
|
402
1022
|
let snapshot;
|
|
403
1023
|
if (entry) {
|
|
404
|
-
|
|
405
|
-
const isolated = { runtime: { getState: () => state } };
|
|
406
|
-
requestLiveTranscriptWindow(isolated, { limit: requested, byteBudget: null, pageBase: 0 });
|
|
407
|
-
windowTranscriptSnapshot(isolated, state);
|
|
408
|
-
isolated.transcriptView.paged = true;
|
|
409
|
-
await loadLiveTranscriptHead(isolated, sessionId, readStoredSession);
|
|
410
|
-
snapshot = windowTranscriptSnapshot(isolated, state);
|
|
1024
|
+
snapshot = await isolatedLiveRows(entry, sessionId, readStoredSession, requested);
|
|
411
1025
|
} else {
|
|
412
|
-
|
|
1026
|
+
// Only the range itself is sliced from the stored history.
|
|
1027
|
+
const slice = await readStoredSession(sessionId, {
|
|
1028
|
+
transcriptItemLimit: requested,
|
|
1029
|
+
transcriptSkip: offset,
|
|
1030
|
+
paging: true,
|
|
1031
|
+
});
|
|
1032
|
+
if (!slice) throw new Error(`session ${sessionId} is not available`);
|
|
1033
|
+
const items = slice.items || [];
|
|
1034
|
+
// A reader that ignores `transcriptSkip` answers the whole window — and
|
|
1035
|
+
// one that holds less than the range reaches says so, rather than an
|
|
1036
|
+
// empty page that never advances.
|
|
1037
|
+
const honored = Object.hasOwn(slice, 'skippedFirstId');
|
|
1038
|
+
if (!honored && offset >= items.length && slice.transcriptHasOlder) {
|
|
1039
|
+
throw new Error('session: requested history is not available in the retained projection');
|
|
1040
|
+
}
|
|
1041
|
+
const end = honored ? items.length : Math.max(0, items.length - offset);
|
|
1042
|
+
const start = Math.max(0, end - limit);
|
|
1043
|
+
return {
|
|
1044
|
+
sessionId,
|
|
1045
|
+
offset,
|
|
1046
|
+
items: sanitizeForWire(items.slice(start, end)),
|
|
1047
|
+
hasOlder: start > 0 || slice.transcriptHasOlder === true,
|
|
1048
|
+
};
|
|
413
1049
|
}
|
|
414
1050
|
if (!snapshot) throw new Error(`session ${sessionId} is not available`);
|
|
415
1051
|
const source = snapshot.items || [];
|