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
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import { createHash } from 'node:crypto';
|
|
19
19
|
|
|
20
|
-
// Serialized (UTF-8 JSON) bytes of the retained PROJECTIONS
|
|
21
|
-
//
|
|
22
|
-
//
|
|
20
|
+
// Serialized (UTF-8 JSON) bytes of the retained PROJECTIONS: windows and pages
|
|
21
|
+
// sliced from a session's display history (which itself stays with the thread
|
|
22
|
+
// that built it); one larger than the whole budget is not retained.
|
|
23
23
|
const STORED_TRANSCRIPT_CACHE_MAX_BYTES = 32 * 1024 * 1024;
|
|
24
24
|
// Coarse filesystems stamp mtime at whole seconds (FAT: two). A write landing
|
|
25
25
|
// inside that window after our read could keep the same stat, so only a file
|
|
@@ -70,22 +70,42 @@ export function nextProjectionStamp() {
|
|
|
70
70
|
return `${stampEpoch}:${stampSequence}`;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
const producedSlices = new WeakSet();
|
|
74
|
+
/** What a `produce` returns for a slice of a display history it may already
|
|
75
|
+
* hold: the value, its serialized `bytes` (measured by the producer) and
|
|
76
|
+
* whether a history was built for it (`historyBuilt`). */
|
|
77
|
+
export function producedSlice(value, { bytes = null, historyBuilt = true } = {}) {
|
|
78
|
+
const slice = { value, bytes, historyBuilt };
|
|
79
|
+
producedSlices.add(slice);
|
|
80
|
+
return slice;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** This realm's stamp epoch (a worker composes history stamps with its
|
|
84
|
+
* caller's). */
|
|
85
|
+
export function projectionStampEpoch() {
|
|
86
|
+
return stampEpoch;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The identity of one display history: its session, record content and
|
|
90
|
+
* sidecar fingerprint, within one process epoch (a restart re-proves held
|
|
91
|
+
* rows by digest). Every slice of the history shares it, whichever thread
|
|
92
|
+
* built it and however often, so held rows stay proven across pages,
|
|
93
|
+
* evictions and worker restarts. */
|
|
94
|
+
export function historyStampOf(sessionId, textHash, textChars, fingerprint, epoch = stampEpoch) {
|
|
95
|
+
const id = createHash('sha256')
|
|
96
|
+
.update(`${sessionId}|${textHash}|${textChars}|${fingerprint}`)
|
|
97
|
+
.digest('base64url')
|
|
98
|
+
.slice(0, 22);
|
|
99
|
+
return `${epoch}:h${id}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The retained entries and their eviction policy: byte/entry budget, paging
|
|
103
|
+
* pins, ghost admission and idle trimming. */
|
|
104
|
+
function createEntryStore({ maxEntries, maxBytes, maxIdleBytes }) {
|
|
80
105
|
// Retain a content digest, not a second full transcript beside its projection.
|
|
81
106
|
// The bound is the size of what is actually retained: the projection.
|
|
82
107
|
/** key -> { textHash, textChars, bytes, group, fingerprint, value, usedAt } (Map order is LRU order). */
|
|
83
108
|
const entries = new Map();
|
|
84
|
-
/** key -> { text, fingerprint, promise } for reads still parsing. */
|
|
85
|
-
const inFlight = new Map();
|
|
86
|
-
/** key -> { fingerprint, fileStat, promise } for reads not yet answered,
|
|
87
|
-
* from before their body is loaded. */
|
|
88
|
-
const reading = new Map();
|
|
89
109
|
/** Keys evicted for space (insertion order = age), bounded. */
|
|
90
110
|
const ghosts = new Set();
|
|
91
111
|
let retainedBytes = 0;
|
|
@@ -104,8 +124,21 @@ export function createStoredTranscriptCache({
|
|
|
104
124
|
};
|
|
105
125
|
const overBudget = (extraEntries = 0, extraBytes = 0) =>
|
|
106
126
|
entries.size + extraEntries > maxEntries || retainedBytes + extraBytes > maxBytes;
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
// A history a reader is paging through (read with `pin` within
|
|
128
|
+
// ACTIVE_ENTRY_MS) stays cached under pressure: every page would otherwise
|
|
129
|
+
// build it again.
|
|
130
|
+
const paging = (entry, now) => now - entry.pinnedAt <= ACTIVE_ENTRY_MS;
|
|
131
|
+
// The newest entry stays (never larger than the budget: remember refuses
|
|
132
|
+
// those). For an unpinned newcomer, entries being paged are not evicted; a
|
|
133
|
+
// pinned one evicts whatever exceeds the budget, pinned or not.
|
|
134
|
+
const prune = (now, keepPaging) => {
|
|
135
|
+
const listed = [...entries];
|
|
136
|
+
const newest = listed.at(-1)?.[0];
|
|
137
|
+
for (const [key, entry] of listed) {
|
|
138
|
+
if (!overBudget()) return;
|
|
139
|
+
if (key === newest || (keepPaging && paging(entry, now))) continue;
|
|
140
|
+
evict(key);
|
|
141
|
+
}
|
|
109
142
|
};
|
|
110
143
|
// A returning ghost may displace only idle entries (oldest first).
|
|
111
144
|
const makeRoomForGhost = (bytes, now) => {
|
|
@@ -123,25 +156,37 @@ export function createStoredTranscriptCache({
|
|
|
123
156
|
for (const other of idle) evict(other);
|
|
124
157
|
return true;
|
|
125
158
|
};
|
|
126
|
-
const remember = (key, group, textChars, textHash, fingerprint, fileStat, value, now) => {
|
|
159
|
+
const remember = (key, group, textChars, textHash, fingerprint, fileStat, value, now, pin, measured) => {
|
|
127
160
|
drop(key);
|
|
128
161
|
// One entry per group: a growing history window replaces its smaller
|
|
129
162
|
// predecessor instead of accumulating beside it.
|
|
130
163
|
if (group !== null) {
|
|
131
164
|
for (const [other, entry] of [...entries]) if (entry.group === group) drop(other);
|
|
132
165
|
}
|
|
133
|
-
|
|
166
|
+
// Values are slices (a window or page), never a whole history; the
|
|
167
|
+
// producer's own measure is used when it has one.
|
|
168
|
+
const bytes = Number.isSafeInteger(measured) && measured >= 0 ? measured : Buffer.byteLength(JSON.stringify(value));
|
|
169
|
+
// Larger than the whole budget: served uncached, pinned or not.
|
|
134
170
|
if (bytes > maxBytes) return;
|
|
135
|
-
if (ghosts.
|
|
136
|
-
|
|
137
|
-
ghosts.
|
|
171
|
+
if (pin) ghosts.delete(key);
|
|
172
|
+
else {
|
|
173
|
+
if (ghosts.has(key)) {
|
|
174
|
+
if (!makeRoomForGhost(bytes, now)) return;
|
|
175
|
+
ghosts.delete(key);
|
|
176
|
+
}
|
|
177
|
+
// Never in place of a history being paged: served uncached instead.
|
|
178
|
+
let pagedBytes = 0;
|
|
179
|
+
for (const entry of entries.values()) if (paging(entry, now)) pagedBytes += entry.bytes;
|
|
180
|
+
if (pagedBytes > 0 && pagedBytes + bytes > maxBytes) return;
|
|
138
181
|
}
|
|
139
|
-
|
|
182
|
+
const pinnedAt = pin ? now : Number.NEGATIVE_INFINITY;
|
|
183
|
+
entries.set(key, { textChars, textHash, bytes, group, fingerprint, fileStat, value, usedAt: now, pinnedAt });
|
|
140
184
|
retainedBytes += bytes;
|
|
141
|
-
prune();
|
|
185
|
+
prune(now, !pin);
|
|
142
186
|
};
|
|
143
|
-
const touch = (key, entry, now) => {
|
|
187
|
+
const touch = (key, entry, now, pin = false) => {
|
|
144
188
|
entry.usedAt = now;
|
|
189
|
+
if (pin) entry.pinnedAt = now;
|
|
145
190
|
entries.delete(key);
|
|
146
191
|
entries.set(key, entry);
|
|
147
192
|
};
|
|
@@ -168,14 +213,59 @@ export function createStoredTranscriptCache({
|
|
|
168
213
|
: null;
|
|
169
214
|
};
|
|
170
215
|
|
|
216
|
+
return {
|
|
217
|
+
get: (key) => entries.get(key),
|
|
218
|
+
remember,
|
|
219
|
+
touch,
|
|
220
|
+
trimIdle,
|
|
221
|
+
entryWithContent,
|
|
222
|
+
release(keyPrefix, now) {
|
|
223
|
+
for (const [key, entry] of entries) {
|
|
224
|
+
if (key.startsWith(keyPrefix)) entry.usedAt = Math.min(entry.usedAt, now - ACTIVE_ENTRY_MS - 1);
|
|
225
|
+
}
|
|
226
|
+
trimIdle(now);
|
|
227
|
+
},
|
|
228
|
+
forget(keyPrefix) {
|
|
229
|
+
for (const key of [...entries.keys()]) {
|
|
230
|
+
if (key.startsWith(keyPrefix)) drop(key);
|
|
231
|
+
}
|
|
232
|
+
for (const key of [...ghosts]) {
|
|
233
|
+
if (key.startsWith(keyPrefix)) ghosts.delete(key);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
clear() {
|
|
237
|
+
entries.clear();
|
|
238
|
+
ghosts.clear();
|
|
239
|
+
retainedBytes = 0;
|
|
240
|
+
},
|
|
241
|
+
stats: () => ({ entries: entries.size, retainedBytes, ghosts: ghosts.size }),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function createStoredTranscriptCache({
|
|
246
|
+
// Bound retained content, not the number of small sessions. An eight-entry
|
|
247
|
+
// LRU reparsed every unchanged record when nine visible sessions refreshed.
|
|
248
|
+
maxEntries = Number.POSITIVE_INFINITY,
|
|
249
|
+
maxBytes = STORED_TRANSCRIPT_CACHE_MAX_BYTES,
|
|
250
|
+
maxIdleBytes = STORED_TRANSCRIPT_IDLE_MAX_BYTES,
|
|
251
|
+
} = {}) {
|
|
252
|
+
const store = createEntryStore({ maxEntries, maxBytes, maxIdleBytes });
|
|
253
|
+
/** key -> { text, fingerprint, promise } for reads still parsing. */
|
|
254
|
+
const inFlight = new Map();
|
|
255
|
+
/** key -> { fingerprint, fileStat, promise } for reads not yet answered,
|
|
256
|
+
* from before their body is loaded. */
|
|
257
|
+
const reading = new Map();
|
|
258
|
+
// Projections produced (here or by `project`), cached or not.
|
|
259
|
+
let builds = 0;
|
|
260
|
+
|
|
171
261
|
// `project(known)` reads, digests and projects the record elsewhere (a
|
|
172
262
|
// worker thread) in one step: `{ textChars, textHash, value }`, or
|
|
173
263
|
// `{ textChars, textHash, same: true }` without a projection when the
|
|
174
264
|
// content is the `known` digest of the cached entry; null when the record
|
|
175
265
|
// cannot be read. Undefined when it could not answer at all.
|
|
176
|
-
async function readProjected({ key, group, fingerprint, fileStat, project, now }) {
|
|
266
|
+
async function readProjected({ key, group, fingerprint, fileStat, project, now, pin }) {
|
|
177
267
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
178
|
-
const cached =
|
|
268
|
+
const cached = store.get(key);
|
|
179
269
|
const known =
|
|
180
270
|
attempt === 0 && cached?.fingerprint === fingerprint
|
|
181
271
|
? { textChars: cached.textChars, textHash: cached.textHash }
|
|
@@ -195,38 +285,52 @@ export function createStoredTranscriptCache({
|
|
|
195
285
|
if (answer === undefined) return undefined;
|
|
196
286
|
if (answer === null || typeof answer !== 'object') return { value: null, hit: false, read: true };
|
|
197
287
|
if (answer.same === true) {
|
|
198
|
-
const entry = entryWithContent(key, fingerprint, answer);
|
|
288
|
+
const entry = store.entryWithContent(key, fingerprint, answer);
|
|
199
289
|
// Evicted meanwhile: project it after all.
|
|
200
290
|
if (!entry) continue;
|
|
201
291
|
entry.fileStat = fileStat;
|
|
202
|
-
touch(key, entry, now);
|
|
292
|
+
store.touch(key, entry, now, pin);
|
|
203
293
|
return { value: entry.value, hit: true, read: true };
|
|
204
294
|
}
|
|
205
295
|
const value = answer.value ?? null;
|
|
296
|
+
// A worker that served the slice from a history it already held built nothing.
|
|
297
|
+
const built = Boolean(value && typeof value === 'object' && answer.historyBuilt !== false);
|
|
298
|
+
if (built) builds += 1;
|
|
206
299
|
if (current && value && typeof value === 'object') {
|
|
207
|
-
remember(
|
|
300
|
+
store.remember(
|
|
301
|
+
key,
|
|
302
|
+
group,
|
|
303
|
+
answer.textChars,
|
|
304
|
+
answer.textHash,
|
|
305
|
+
fingerprint,
|
|
306
|
+
fileStat,
|
|
307
|
+
value,
|
|
308
|
+
now,
|
|
309
|
+
pin,
|
|
310
|
+
answer.bytes
|
|
311
|
+
);
|
|
208
312
|
}
|
|
209
|
-
return { value, hit: false, read: true };
|
|
313
|
+
return { value, hit: false, read: true, built };
|
|
210
314
|
}
|
|
211
315
|
return undefined;
|
|
212
316
|
}
|
|
213
317
|
|
|
214
|
-
async function readFresh({ key, group, fingerprint, fileStat, loadText, produce, project, now }) {
|
|
318
|
+
async function readFresh({ key, group, fingerprint, fileStat, loadText, produce, project, now, pin }) {
|
|
215
319
|
if (typeof project === 'function') {
|
|
216
|
-
const answered = await readProjected({ key, group, fingerprint, fileStat, project, now });
|
|
320
|
+
const answered = await readProjected({ key, group, fingerprint, fileStat, project, now, pin });
|
|
217
321
|
if (answered !== undefined) return answered;
|
|
218
322
|
// It could not answer (worker unavailable): load and produce here.
|
|
219
323
|
}
|
|
220
|
-
const cached =
|
|
324
|
+
const cached = store.get(key);
|
|
221
325
|
// `loadText` may answer asynchronously (a large record is read without
|
|
222
326
|
// blocking the loop); a plain string is unchanged.
|
|
223
327
|
let text = loadText();
|
|
224
328
|
if (typeof text?.then === 'function') text = await text;
|
|
225
329
|
if (typeof text !== 'string') return { value: null, hit: false, read: true };
|
|
226
330
|
const textHash = transcriptTextHash(text);
|
|
227
|
-
if (cached && entryWithContent(key, fingerprint, { textChars: text.length, textHash }) === cached) {
|
|
331
|
+
if (cached && store.entryWithContent(key, fingerprint, { textChars: text.length, textHash }) === cached) {
|
|
228
332
|
cached.fileStat = fileStat;
|
|
229
|
-
touch(key, cached, now);
|
|
333
|
+
store.touch(key, cached, now, pin);
|
|
230
334
|
return { value: cached.value, hit: true, read: true };
|
|
231
335
|
}
|
|
232
336
|
const pending = inFlight.get(key);
|
|
@@ -234,17 +338,23 @@ export function createStoredTranscriptCache({
|
|
|
234
338
|
return { value: await pending.promise, hit: true, read: true };
|
|
235
339
|
}
|
|
236
340
|
const record = { text, fingerprint, promise: null };
|
|
341
|
+
let built = false;
|
|
237
342
|
const promise = (async () => {
|
|
238
|
-
const
|
|
343
|
+
const produced = await produce(text);
|
|
344
|
+
const slice = producedSlices.has(produced) ? produced : null;
|
|
345
|
+
const value = slice ? slice.value : produced;
|
|
346
|
+
built = Boolean(value && typeof value === 'object' && slice?.historyBuilt !== false);
|
|
347
|
+
if (built) builds += 1;
|
|
239
348
|
if (inFlight.get(key) === record && value && typeof value === 'object') {
|
|
240
|
-
remember(key, group, text.length, textHash, fingerprint, fileStat, value, now);
|
|
349
|
+
store.remember(key, group, text.length, textHash, fingerprint, fileStat, value, now, pin, slice?.bytes);
|
|
241
350
|
}
|
|
242
351
|
return value;
|
|
243
352
|
})();
|
|
244
353
|
record.promise = promise;
|
|
245
354
|
inFlight.set(key, record);
|
|
246
355
|
try {
|
|
247
|
-
|
|
356
|
+
const value = await promise;
|
|
357
|
+
return { value, hit: false, read: true, built };
|
|
248
358
|
} finally {
|
|
249
359
|
if (inFlight.get(key) === record) inFlight.delete(key);
|
|
250
360
|
}
|
|
@@ -256,7 +366,9 @@ export function createStoredTranscriptCache({
|
|
|
256
366
|
* `project` run only when stat alone cannot vouch for the entry.
|
|
257
367
|
* Concurrent callers with the same content share one parse; callers of
|
|
258
368
|
* one unchanged, settled file share one read before it is even loaded.
|
|
259
|
-
* Entries naming the same `group` replace each other.
|
|
369
|
+
* Entries naming the same `group` replace each other. `pin`: a reader is
|
|
370
|
+
* paging through this entry — it is cached however full the cache is,
|
|
371
|
+
* and for ACTIVE_ENTRY_MS no unpinned newcomer evicts it. */
|
|
260
372
|
async read({
|
|
261
373
|
key,
|
|
262
374
|
group = null,
|
|
@@ -266,11 +378,12 @@ export function createStoredTranscriptCache({
|
|
|
266
378
|
produce,
|
|
267
379
|
project = null,
|
|
268
380
|
now = Date.now(),
|
|
381
|
+
pin = false,
|
|
269
382
|
}) {
|
|
270
|
-
trimIdle(now);
|
|
271
|
-
const cached =
|
|
383
|
+
store.trimIdle(now);
|
|
384
|
+
const cached = store.get(key);
|
|
272
385
|
if (cached && cached.fingerprint === fingerprint && settledSameFile(cached.fileStat, fileStat, now)) {
|
|
273
|
-
touch(key, cached, now);
|
|
386
|
+
store.touch(key, cached, now, pin);
|
|
274
387
|
return { value: cached.value, hit: true, read: false };
|
|
275
388
|
}
|
|
276
389
|
const joined = reading.get(key);
|
|
@@ -280,7 +393,7 @@ export function createStoredTranscriptCache({
|
|
|
280
393
|
if (value) return { value, hit: true, read: false };
|
|
281
394
|
}
|
|
282
395
|
const record = { fingerprint, fileStat, promise: null };
|
|
283
|
-
record.promise = readFresh({ key, group, fingerprint, fileStat, loadText, produce, project, now });
|
|
396
|
+
record.promise = readFresh({ key, group, fingerprint, fileStat, loadText, produce, project, now, pin });
|
|
284
397
|
reading.set(key, record);
|
|
285
398
|
try {
|
|
286
399
|
return await record.promise;
|
|
@@ -291,10 +404,7 @@ export function createStoredTranscriptCache({
|
|
|
291
404
|
/** The last view of these projections left: they turn idle now and stay
|
|
292
405
|
* only within the idle budget, ready for a revisit. */
|
|
293
406
|
release(keyPrefix, now = Date.now()) {
|
|
294
|
-
|
|
295
|
-
if (key.startsWith(keyPrefix)) entry.usedAt = Math.min(entry.usedAt, now - ACTIVE_ENTRY_MS - 1);
|
|
296
|
-
}
|
|
297
|
-
trimIdle(now);
|
|
407
|
+
store.release(keyPrefix, now);
|
|
298
408
|
},
|
|
299
409
|
forget(keyPrefix) {
|
|
300
410
|
for (const pending of [inFlight, reading]) {
|
|
@@ -302,28 +412,17 @@ export function createStoredTranscriptCache({
|
|
|
302
412
|
if (key.startsWith(keyPrefix)) pending.delete(key);
|
|
303
413
|
}
|
|
304
414
|
}
|
|
305
|
-
|
|
306
|
-
if (key.startsWith(keyPrefix)) drop(key);
|
|
307
|
-
}
|
|
308
|
-
for (const key of [...ghosts]) {
|
|
309
|
-
if (key.startsWith(keyPrefix)) ghosts.delete(key);
|
|
310
|
-
}
|
|
415
|
+
store.forget(keyPrefix);
|
|
311
416
|
},
|
|
312
417
|
clear() {
|
|
313
|
-
|
|
418
|
+
store.clear();
|
|
314
419
|
inFlight.clear();
|
|
315
420
|
reading.clear();
|
|
316
|
-
|
|
317
|
-
retainedBytes = 0;
|
|
421
|
+
builds = 0;
|
|
318
422
|
},
|
|
319
423
|
stats() {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
retainedBytes,
|
|
323
|
-
inFlight: inFlight.size,
|
|
324
|
-
reading: reading.size,
|
|
325
|
-
ghosts: ghosts.size,
|
|
326
|
-
};
|
|
424
|
+
const { entries, retainedBytes, ghosts } = store.stats();
|
|
425
|
+
return { entries, retainedBytes, inFlight: inFlight.size, reading: reading.size, ghosts, builds };
|
|
327
426
|
},
|
|
328
427
|
};
|
|
329
428
|
}
|
|
@@ -107,27 +107,186 @@ export async function prepareStoredTranscriptBase(sessionId, doc, options) {
|
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
/**
|
|
110
|
+
/** The digest a paged caller proves its held rows with: over their wire form,
|
|
111
|
+
* as the caller serializes them. */
|
|
112
|
+
export async function heldRowsDigester() {
|
|
113
|
+
const [{ sanitizeForWire }, { transcriptItemsDigest }] = await Promise.all([
|
|
114
|
+
import('../../../../standalone/session-wire-values.mjs'),
|
|
115
|
+
import('../../../../standalone/session-state-patch.mjs'),
|
|
116
|
+
]);
|
|
117
|
+
return (rows) => transcriptItemsDigest(rows.map((row) => sanitizeForWire(row, 2) ?? null));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The rows a page reveals never go below this many whatever their size, so
|
|
121
|
+
* paging always progresses (the caller's own floor). */
|
|
122
|
+
const PAGE_MIN_ROWS = 8;
|
|
123
|
+
|
|
124
|
+
/** The slice of a whole display history (windowStoredTranscript with
|
|
125
|
+
* `history`) one read receives (`request`):
|
|
126
|
+
* - `held` (`{ firstId, count, digest }`, the newest rows a paged caller
|
|
127
|
+
* holds): when the history's newest `held.count` rows are those rows (same
|
|
128
|
+
* first id and — unless `proven` — the same `digestOf` digest), only the
|
|
129
|
+
* rows above them: at most `maxRows` (and `limit - held.count`) within
|
|
130
|
+
* `byteBudget` (`rowBytes(index)`), never fewer than PAGE_MIN_ROWS, with
|
|
131
|
+
* `heldProven`. Unproven: a tail of at most `maxRows` rows instead
|
|
132
|
+
* (`heldProven: false`).
|
|
133
|
+
* - otherwise its newest `limit` rows but the newest `skip` (with
|
|
134
|
+
* `skippedFirstId`, the oldest skipped row) — or, without a finite limit
|
|
135
|
+
* (the legacy whole read), every restored row and none of the compaction
|
|
136
|
+
* archive.
|
|
137
|
+
* `windowStamp` names the slice within the history (the caller composes the
|
|
138
|
+
* projection stamp). Slicing only: whatever the depth, a slice carries only
|
|
139
|
+
* its own rows, and the history never leaves the thread that built it. */
|
|
140
|
+
export function storedHistorySlice(history, request = {}, { digestOf = null, rowBytes = null } = {}) {
|
|
141
|
+
if (!history || typeof history !== 'object' || !Array.isArray(history.items)) return history;
|
|
142
|
+
const { transcriptArchivedRows: archivedRows = 0, items, projectionStamp: _stamp, ...fields } = history;
|
|
143
|
+
const sizeOf =
|
|
144
|
+
typeof rowBytes === 'function' ? rowBytes : (index) => Buffer.byteLength(JSON.stringify(items[index] ?? null));
|
|
145
|
+
if (request.cursor) return historyCursorSlice(fields, items, request.cursor, history.transcriptHasOlder, sizeOf);
|
|
146
|
+
const itemLimit = Number(request.limit);
|
|
147
|
+
if (!Number.isFinite(itemLimit)) {
|
|
148
|
+
return {
|
|
149
|
+
...fields,
|
|
150
|
+
windowStamp: 'whole',
|
|
151
|
+
items: archivedRows > 0 ? items.slice(archivedRows) : items.slice(),
|
|
152
|
+
transcriptHasOlder: false,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const limit = Math.max(0, Math.floor(itemLimit));
|
|
156
|
+
const held = request.held;
|
|
157
|
+
if (held) {
|
|
158
|
+
const maxRows = Math.max(1, Math.floor(Number(request.maxRows) || limit));
|
|
159
|
+
const end = items.length - held.count;
|
|
160
|
+
const holds =
|
|
161
|
+
end >= 0 &&
|
|
162
|
+
items[end]?.id === held.firstId &&
|
|
163
|
+
(request.proven === true || (typeof digestOf === 'function' && digestOf(items.slice(end)) === held.digest));
|
|
164
|
+
if (!holds) {
|
|
165
|
+
// A fresh tail of one answer, within the same byte budget (and floor).
|
|
166
|
+
const start = budgetedEdge(items.length, -1, items.length, Math.min(limit, maxRows), request.byteBudget, sizeOf);
|
|
167
|
+
return {
|
|
168
|
+
...fields,
|
|
169
|
+
windowStamp: `${items.length - start}`,
|
|
170
|
+
items: items.slice(start),
|
|
171
|
+
transcriptHasOlder: start > 0 || history.transcriptHasOlder === true,
|
|
172
|
+
heldProven: false,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const rows = Math.max(0, Math.min(maxRows, limit - held.count));
|
|
176
|
+
const start = budgetedEdge(end, -1, items.length, rows, request.byteBudget, sizeOf);
|
|
177
|
+
return {
|
|
178
|
+
...fields,
|
|
179
|
+
windowStamp: `${items.length - start}@${held.count}`,
|
|
180
|
+
items: items.slice(start, end),
|
|
181
|
+
transcriptHasOlder: start > 0 || history.transcriptHasOlder === true,
|
|
182
|
+
skippedFirstId: held.firstId,
|
|
183
|
+
heldProven: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const skipped = Math.max(0, Math.min(Math.floor(Number(request.skip) || 0), limit, items.length));
|
|
187
|
+
const end = items.length - skipped;
|
|
188
|
+
const start = Math.max(0, items.length - limit);
|
|
189
|
+
return {
|
|
190
|
+
...fields,
|
|
191
|
+
windowStamp: skipped ? `${limit}@${skipped}` : `${limit}`,
|
|
192
|
+
items: items.slice(start, end),
|
|
193
|
+
transcriptHasOlder: start > 0 || history.transcriptHasOlder === true,
|
|
194
|
+
...(skipped ? { skippedFirstId: items[end]?.id ?? null } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The far edge of a run of at most `rows` rows within `byteBudget`
|
|
199
|
+
* (`sizeOf(index)`), never fewer than PAGE_MIN_ROWS whatever their size:
|
|
200
|
+
* older (`step` -1) ending just before index `from`, or newer (`step` +1)
|
|
201
|
+
* starting at it. */
|
|
202
|
+
function budgetedEdge(from, step, length, rows, byteBudget, sizeOf) {
|
|
203
|
+
const budget = Number(byteBudget) > 0 ? Number(byteBudget) : null;
|
|
204
|
+
const floor = Math.min(rows, PAGE_MIN_ROWS);
|
|
205
|
+
let edge = from;
|
|
206
|
+
let taken = 0;
|
|
207
|
+
let bytes = 0;
|
|
208
|
+
while (taken < rows) {
|
|
209
|
+
const index = step < 0 ? edge - 1 : edge;
|
|
210
|
+
if (index < 0 || index >= length) break;
|
|
211
|
+
const next = budget ? sizeOf(index) : 0;
|
|
212
|
+
if (budget && taken >= floor && bytes + next > budget) break;
|
|
213
|
+
bytes += next;
|
|
214
|
+
taken += 1;
|
|
215
|
+
edge += step;
|
|
216
|
+
}
|
|
217
|
+
return edge;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The most rows one historical-position read carries, whatever it asks. */
|
|
221
|
+
export const CURSOR_PAGE_MAX_ROWS = 512;
|
|
222
|
+
|
|
223
|
+
/** A cursor's page size: 1 to CURSOR_PAGE_MAX_ROWS rows. */
|
|
224
|
+
export function cursorPageRows(maxRows) {
|
|
225
|
+
return Math.min(CURSOR_PAGE_MAX_ROWS, Math.max(1, Math.floor(Number(maxRows) || 1)));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** A historical-position read (`cursor`: `{ before | after, maxRows,
|
|
229
|
+
* byteBudget }`): the rows directly older than row `before`, or newer than
|
|
230
|
+
* row `after`, at most `maxRows` within `byteBudget` (PAGE_MIN_ROWS floor),
|
|
231
|
+
* with whether rows remain past them each way. Rows are located by their
|
|
232
|
+
* stable id, so a page stays valid while the history only grows; a row the
|
|
233
|
+
* history no longer holds (compaction, rewind, clear) answers
|
|
234
|
+
* `cursorMissing`. */
|
|
235
|
+
function historyCursorSlice(fields, items, cursor, olderBeyond, sizeOf) {
|
|
236
|
+
const older = cursor.before != null;
|
|
237
|
+
const id = older ? cursor.before : cursor.after;
|
|
238
|
+
const at = items.findIndex((item) => item?.id === id);
|
|
239
|
+
if (at < 0) return { ...fields, windowStamp: 'cursor', items: [], cursorMissing: true };
|
|
240
|
+
const rows = cursorPageRows(cursor.maxRows);
|
|
241
|
+
if (older) {
|
|
242
|
+
const start = budgetedEdge(at, -1, items.length, rows, cursor.byteBudget, sizeOf);
|
|
243
|
+
return {
|
|
244
|
+
...fields,
|
|
245
|
+
windowStamp: 'cursor',
|
|
246
|
+
items: items.slice(start, at),
|
|
247
|
+
transcriptHasOlder: start > 0 || olderBeyond === true,
|
|
248
|
+
transcriptHasNewer: true,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const end = budgetedEdge(at + 1, 1, items.length, rows, cursor.byteBudget, sizeOf);
|
|
252
|
+
return {
|
|
253
|
+
...fields,
|
|
254
|
+
windowStamp: 'cursor',
|
|
255
|
+
items: items.slice(at + 1, end),
|
|
256
|
+
transcriptHasOlder: true,
|
|
257
|
+
transcriptHasNewer: end < items.length,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** One transcript window (`options.itemLimit`, default whole) of a base.
|
|
262
|
+
* `options.history`: the whole display history instead — every restored row
|
|
263
|
+
* after every row of the visible compaction archive, with
|
|
264
|
+
* `transcriptArchivedRows` naming how many lead it. Every window is a tail
|
|
265
|
+
* of it (storedHistoryWindow). */
|
|
111
266
|
export async function windowStoredTranscript(sessionId, base, options) {
|
|
112
267
|
const { session, messages, contextFields } = base;
|
|
113
268
|
const { restoreTranscriptItems } = await import('./transcript-restore/restore.mjs');
|
|
269
|
+
const history = options.history === true;
|
|
114
270
|
// Restoring one item past a finite limit proves whether older history exists
|
|
115
271
|
// without projecting it; restore ids are message-positional, so the kept
|
|
116
272
|
// tail is identical either way.
|
|
117
|
-
const itemLimit = Number.isFinite(options.itemLimit) ? options.itemLimit : Number.POSITIVE_INFINITY;
|
|
273
|
+
const itemLimit = !history && Number.isFinite(options.itemLimit) ? options.itemLimit : Number.POSITIVE_INFINITY;
|
|
118
274
|
const restored = restoreTranscriptItems(messages, { sessionId, itemLimit: itemLimit + 1 });
|
|
119
275
|
let transcriptHasOlder = restored.length > itemLimit;
|
|
120
276
|
let items = transcriptHasOlder ? restored.slice(-itemLimit) : restored;
|
|
277
|
+
let archivedRows = 0;
|
|
121
278
|
// A paged window continues past the first stored message into the
|
|
122
279
|
// display-only compaction archive (newest segment first). A legacy whole
|
|
123
280
|
// read never receives it.
|
|
124
|
-
const archiveRange =
|
|
281
|
+
const archiveRange =
|
|
282
|
+
history || (Number.isFinite(itemLimit) && !transcriptHasOlder) ? compactionArchiveRange(session) : null;
|
|
125
283
|
if (archiveRange) {
|
|
126
284
|
const archived = await readCompactionArchivePage(join(dataDir(), 'sessions'), sessionId, {
|
|
127
285
|
...archiveRange,
|
|
128
|
-
limit: itemLimit - restored.length,
|
|
286
|
+
limit: history ? Number.POSITIVE_INFINITY : itemLimit - restored.length,
|
|
129
287
|
});
|
|
130
288
|
if (archived.items.length) items = [...archived.items, ...restored];
|
|
289
|
+
archivedRows = archived.items.length;
|
|
131
290
|
transcriptHasOlder = archived.hasOlder;
|
|
132
291
|
}
|
|
133
292
|
return {
|
|
@@ -136,6 +295,7 @@ export async function windowStoredTranscript(sessionId, base, options) {
|
|
|
136
295
|
...(options.includeMessages ? { messages } : {}),
|
|
137
296
|
items,
|
|
138
297
|
transcriptHasOlder,
|
|
298
|
+
...(history ? { transcriptArchivedRows: archivedRows } : {}),
|
|
139
299
|
provider: session.provider || '',
|
|
140
300
|
model: session.model || '',
|
|
141
301
|
effort: session.effort || '',
|