mixdog 1.1.2 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/package.json +2 -1
- package/scripts/lib/run-node-tests.mjs +9 -0
- package/scripts/prune-desktop-runtime.mjs +3 -0
- package/scripts/test-direct.mjs +4 -2
- package/src/defaults/shipped-history.json +10 -0
- package/src/defaults/skills/pptx/SKILL.md +6 -6
- package/src/defaults/skills/pptx/references/review.md +6 -4
- package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
- package/src/defaults/skills/setup/references/actions.md +18 -5
- package/src/defaults/skills/visualize/SKILL.md +160 -0
- package/src/defaults/skills/visualize/references/craft.md +88 -0
- package/src/headless-command.mjs +56 -3
- package/src/headless-exec.mjs +17 -1
- package/src/help.mjs +2 -0
- package/src/rules/shared/60-verification.md +3 -1
- package/src/rules/shared/70-delivery.md +4 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
- package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
- package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
- package/src/runtime/agent/orchestrator/config.mjs +19 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +133 -15
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +55 -8
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +86 -8
- package/src/runtime/agent/orchestrator/mcp/client.mjs +474 -164
- package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
- package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -12
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +21 -6
- package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
- package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +82 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +556 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +15 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -7
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +15 -11
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
- package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +278 -84
- package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +24 -13
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
- package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +21 -10
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +9 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +33 -22
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +23 -12
- package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +202 -374
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +48 -9
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +203 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -68
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +20 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
- package/src/runtime/attachments/store.d.mts +3 -0
- package/src/runtime/attachments/store.mjs +38 -0
- package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
- package/src/runtime/channels/lib/worker-main.mjs +17 -14
- package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
- package/src/runtime/effort-judge/judge-client.mjs +56 -18
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +2 -2
- package/src/runtime/shared/atomic-file.mjs +33 -6
- package/src/runtime/shared/best-effort-stderr.mjs +8 -0
- package/src/runtime/shared/code-graph-state.mjs +12 -0
- package/src/runtime/shared/defaults-separation.mjs +2 -0
- package/src/runtime/shared/extension-scopes.mjs +5 -3
- package/src/runtime/shared/file-lock.mjs +37 -11
- package/src/runtime/shared/git-runner-worker.mjs +3 -2
- package/src/runtime/shared/git-runner.mjs +4 -3
- package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
- package/src/runtime/shared/llm/default-context-window.mjs +6 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
- package/src/runtime/shared/llm/model-catalog.mjs +240 -130
- package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +162 -0
- package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
- package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
- package/src/runtime/shared/model-proxy-mode.mjs +19 -0
- package/src/runtime/shared/plugin-manifest.mjs +18 -9
- package/src/runtime/shared/pristine-execution-contract.json +9 -2
- package/src/runtime/shared/pristine-execution.mjs +28 -4
- package/src/runtime/shared/provider-accounts.mjs +125 -21
- package/src/runtime/shared/provider-api-key.mjs +33 -1
- package/src/runtime/shared/schedule-model-ref.mjs +11 -2
- package/src/runtime/shared/session-usage-totals.mjs +19 -0
- package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
- package/src/runtime/shared/shell-display.mjs +9 -3
- package/src/runtime/shared/singleton-owner.mjs +36 -6
- package/src/runtime/shared/skill-mentions.d.mts +10 -0
- package/src/runtime/shared/skill-mentions.mjs +133 -0
- package/src/runtime/shared/skill-script-runtime.mjs +264 -0
- package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
- package/src/runtime/shared/tool-primitives.mjs +35 -2
- package/src/runtime/shared/tool-surface.mjs +7 -0
- package/src/runtime/shared/tool-work-units.mjs +14 -0
- package/src/runtime/shared/transcript-metadata.mjs +17 -1
- package/src/runtime/shared/turn-changes.mjs +791 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
- package/src/runtime/shared/turn-snapshot.mjs +690 -81
- package/src/runtime/shared/turn-worktree-snapshot.mjs +185 -15
- package/src/runtime/shared/worker-requests.mjs +15 -2
- package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
- package/src/runtime/shared/worktrees/contract.mjs +551 -0
- package/src/runtime/shared/worktrees/disk.mjs +57 -0
- package/src/runtime/shared/worktrees/git.mjs +247 -0
- package/src/runtime/shared/worktrees/include.mjs +480 -0
- package/src/runtime/shared/worktrees/lock.mjs +46 -0
- package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
- package/src/runtime/shared/worktrees/manager.mjs +1215 -0
- package/src/runtime/shared/worktrees/merge.mjs +182 -0
- package/src/runtime/shared/worktrees/owner.mjs +46 -0
- package/src/runtime/shared/worktrees/paths.mjs +62 -0
- package/src/runtime/shared/worktrees/registry.mjs +110 -0
- package/src/runtime/shared/worktrees/settings.mjs +84 -0
- package/src/runtime/shared/worktrees/slug.mjs +62 -0
- package/src/runtime/shared/worktrees/test-support.mjs +96 -0
- package/src/runtime/visualize-bridge/client.mjs +181 -0
- package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
- package/src/session-runtime/automation-agents.mjs +8 -2
- package/src/session-runtime/boot/apis.mjs +90 -0
- package/src/session-runtime/boot/config.mjs +7 -2
- package/src/session-runtime/boot/facade.mjs +21 -0
- package/src/session-runtime/boot/infrastructure.mjs +30 -18
- package/src/session-runtime/boot/session.mjs +1 -0
- package/src/session-runtime/boot/tools.mjs +4 -0
- package/src/session-runtime/config-lifecycle.mjs +10 -1
- package/src/session-runtime/conversation-import/discovery.mjs +698 -0
- package/src/session-runtime/conversation-import/service.mjs +369 -0
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +21 -6
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
- package/src/session-runtime/global-extensions.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
- package/src/session-runtime/internal-tool-executor.mjs +9 -1
- package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
- package/src/session-runtime/mcp-glue/connect.mjs +68 -16
- package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +122 -33
- package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
- package/src/session-runtime/mcp-glue.mjs +22 -2
- package/src/session-runtime/model-route/route-persist.mjs +3 -0
- package/src/session-runtime/model-route/set-route.mjs +15 -2
- package/src/session-runtime/new-session-config.mjs +3 -2
- package/src/session-runtime/notification-bus.mjs +18 -3
- package/src/session-runtime/provider-auth/accounts.mjs +33 -15
- package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
- package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
- package/src/session-runtime/provider-readiness.mjs +16 -10
- package/src/session-runtime/resource-extension-sync.mjs +45 -7
- package/src/session-runtime/resource-mcp-api.mjs +121 -84
- package/src/session-runtime/resource-plugins-api.mjs +29 -41
- package/src/session-runtime/resource-scope-decorate.mjs +6 -8
- package/src/session-runtime/resource-skills-api.mjs +23 -2
- package/src/session-runtime/route-resolution.mjs +10 -1
- package/src/session-runtime/route-state.mjs +16 -2
- package/src/session-runtime/runtime-core.mjs +5 -0
- package/src/session-runtime/runtime-facade.mjs +8 -0
- package/src/session-runtime/runtime-review-api.mjs +9 -0
- package/src/session-runtime/schedule-session-run.mjs +1 -1
- package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
- package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
- package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
- package/src/session-runtime/services/agent-tool.mjs +1 -0
- package/src/session-runtime/services/channel-session-router.mjs +4 -1
- package/src/session-runtime/services/daemon-candidate.mjs +3 -2
- package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
- package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
- package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
- package/src/session-runtime/services/provider-admin.mjs +64 -18
- package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
- package/src/session-runtime/session-create/session-options.mjs +10 -1
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/settings-api.mjs +4 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
- package/src/session-runtime/settings-worktree-api.mjs +17 -0
- package/src/session-runtime/setup-tool/executor.mjs +23 -5
- package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +4 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +38 -4
- package/src/session-runtime/skills-api.mjs +7 -3
- package/src/session-runtime/turn/agent-ops.mjs +12 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +14 -3
- package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
- package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
- package/src/session-runtime/turn/turn-run.mjs +6 -7
- package/src/session-runtime/webhook-session-run.mjs +16 -4
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
- package/src/session-runtime/workspace/admission-gate.mjs +197 -0
- package/src/session-runtime/workspace/project-branches.mjs +136 -0
- package/src/session-runtime/workspace/transition-steps.mjs +364 -0
- package/src/session-runtime/workspace/transitions.mjs +651 -0
- package/src/session-runtime/workspace/workspace-state.mjs +112 -0
- package/src/skill-script-entry.mjs +36 -0
- package/src/standalone/channel-transport/http-routes.mjs +17 -0
- package/src/standalone/channel-transport.mjs +13 -1
- package/src/standalone/daemon-desktop-runtime.mjs +1 -0
- package/src/standalone/daemon-owner.mjs +372 -0
- package/src/standalone/daemon-stored-session-views.mjs +15 -4
- package/src/standalone/daemon.mjs +179 -8
- package/src/standalone/desktop-service-registry.mjs +20 -0
- package/src/standalone/local-session-runtime.mjs +11 -0
- package/src/standalone/session-client/attachment-calls.mjs +3 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
- package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
- package/src/standalone/session-client.mjs +104 -13
- package/src/standalone/session-protocol.mjs +21 -0
- package/src/standalone/session-proxy.mjs +3 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
- package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
- package/src/standalone/session-service/projection/revisions.mjs +2 -0
- package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
- package/src/standalone/session-service/projection/workspace.mjs +14 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +17 -4
- package/src/standalone/session-transport/call-route.mjs +13 -0
- package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
- package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
- package/src/standalone/session-transport/client-registry.mjs +20 -3
- package/src/standalone/session-transport/routes.mjs +79 -2
- package/src/standalone/session-transport/stack.mjs +2 -0
- package/src/standalone/session-transport.mjs +24 -1
- package/src/tui/App.jsx +9 -1
- package/src/tui/app/app-view/floating-panel.jsx +9 -1
- package/src/tui/app/app-view.jsx +6 -0
- package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
- package/src/tui/app/message-selector.mjs +2 -1
- package/src/tui/app/model-options.mjs +37 -5
- package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
- package/src/tui/app/model-picker.mjs +4 -2
- package/src/tui/app/skill-mention-palette.mjs +88 -0
- package/src/tui/app/slash-palette.mjs +23 -7
- package/src/tui/app/transcript-row-estimate.mjs +11 -3
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
- package/src/tui/app/use-prompt-state.mjs +5 -0
- package/src/tui/components/Message.jsx +22 -1
- package/src/tui/components/PromptInput.jsx +21 -3
- package/src/tui/components/SlashCommandPalette.jsx +12 -3
- package/src/tui/components/TranscriptItem.jsx +9 -1
- package/src/tui/components/prompt-input/key-handler.mjs +21 -8
- package/src/tui/components/user-message-skills.mjs +38 -0
- package/src/tui/dist/index.mjs +839 -207
- package/src/tui/index.jsx +10 -2
- package/src/tui/session/compaction-view.mjs +147 -0
- package/src/tui/session/context-state.mjs +6 -0
- package/src/tui/session/initial-state.mjs +3 -0
- package/src/tui/session/queue-helpers.mjs +7 -2
- package/src/tui/session/quota-resume.mjs +2 -1
- package/src/tui/session/session-api/commands.mjs +10 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/extensions.mjs +4 -2
- package/src/tui/session/session-api/intake/rewind.mjs +2 -1
- package/src/tui/session/session-api/integrations.mjs +17 -0
- package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
- package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
- package/src/tui/session/session-api/routes.mjs +10 -1
- package/src/tui/session/session-api/settings.mjs +9 -0
- package/src/tui/session/session-api/worktree.mjs +60 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +31 -12
- package/src/tui/session/session-flow/drain.mjs +27 -3
- package/src/tui/session/session-flow/session-reset.mjs +4 -3
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/transcript-intake.mjs +14 -5
- package/src/tui/session/tui-steering-persist.mjs +25 -9
- package/src/tui/session/turn-stream.mjs +32 -4
- package/src/tui/session/turn-tool-cards.mjs +10 -2
- package/src/tui/session/turn.mjs +56 -10
- package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
- package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
|
@@ -13,6 +13,7 @@ import { createHash } from 'node:crypto';
|
|
|
13
13
|
import { writeJsonAtomicSync } from '../../../../shared/atomic-file.mjs';
|
|
14
14
|
import { resolvePluginData } from '../../../../shared/plugin-paths.mjs';
|
|
15
15
|
import { setBoundedTextCacheEntry } from './text-cache-budget.mjs';
|
|
16
|
+
import { canCacheReadFile } from '../../tools/builtin/read-cache-policy.mjs';
|
|
16
17
|
|
|
17
18
|
// Entry cap: MIXDOG_PREFETCH_CACHE_MAX env var, default 200.
|
|
18
19
|
const _envCap = Number(process.env.MIXDOG_PREFETCH_CACHE_MAX);
|
|
@@ -110,12 +111,14 @@ function rememberPrefetch(absPath, entry) {
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
export function capturePrefetchCacheState(absPath) {
|
|
113
|
-
|
|
114
|
+
const stat = _statTuple(_normalizeCacheKey(absPath));
|
|
115
|
+
return canCacheReadFile(stat) ? stat : null;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
/**
|
|
117
119
|
* Look up a cached prefetch result for `absPath`. Stat-validates the file on
|
|
118
|
-
* every hit. Returns null on miss, TTL expiry,
|
|
120
|
+
* every hit. Returns null on miss, TTL expiry, stat mismatch, or a file
|
|
121
|
+
* outside the prefix budget, including a persisted legacy row. Returns
|
|
119
122
|
* `{ content, ts }` on a fresh hit.
|
|
120
123
|
*/
|
|
121
124
|
export function tryPrefetchCached(absPath) {
|
|
@@ -135,7 +138,7 @@ export function tryPrefetchCached(absPath) {
|
|
|
135
138
|
return null;
|
|
136
139
|
}
|
|
137
140
|
const fresh = _statTuple(absPath);
|
|
138
|
-
if (!_statEqual(entry.stat, fresh)) {
|
|
141
|
+
if (!_statEqual(entry.stat, fresh) || !canCacheReadFile(fresh)) {
|
|
139
142
|
_prefetchCache.delete(absPath);
|
|
140
143
|
_deleteDiskEntry(absPath);
|
|
141
144
|
return null;
|
|
@@ -152,7 +155,8 @@ export function tryPrefetchCached(absPath) {
|
|
|
152
155
|
|
|
153
156
|
/**
|
|
154
157
|
* Store a per-file prefetch result. Silently skips empty or error-prefixed
|
|
155
|
-
* content so only clean read
|
|
158
|
+
* content, and files outside the prefix budget, so only clean bounded read
|
|
159
|
+
* output enters the cache.
|
|
156
160
|
* Async callers must pass the state captured before reading; null disables
|
|
157
161
|
* insertion. Omission is reserved for synchronous current-body insertion.
|
|
158
162
|
*/
|
|
@@ -162,7 +166,7 @@ export function setPrefetchCached(absPath, content, readState) {
|
|
|
162
166
|
if (classifyResultKind(content) === 'error') return;
|
|
163
167
|
absPath = _normalizeCacheKey(absPath);
|
|
164
168
|
const stat = _statTuple(absPath);
|
|
165
|
-
if (!stat) return;
|
|
169
|
+
if (!stat || !canCacheReadFile(stat)) return;
|
|
166
170
|
// Async readers supply the version observed before execution. Never label
|
|
167
171
|
// an older body with the version that happens to exist at insertion time.
|
|
168
172
|
if (readState !== undefined && !_statEqual(readState, stat)) return;
|
|
@@ -5,6 +5,7 @@ import { clearScopedToolsForSession } from './scoped-cache.mjs';
|
|
|
5
5
|
import { registerSessionPurgeHook } from '../store.mjs';
|
|
6
6
|
import { releaseReadSnapshotScope } from '../../tools/builtin/snapshot-store.mjs';
|
|
7
7
|
import { setBoundedTextCacheEntry } from './text-cache-budget.mjs';
|
|
8
|
+
import { canCacheReadFile } from '../../tools/builtin/read-cache-policy.mjs';
|
|
8
9
|
|
|
9
10
|
const MAX_PER_SESSION = 100;
|
|
10
11
|
|
|
@@ -114,6 +115,12 @@ function _arrayStatsValid(statsByAbs) {
|
|
|
114
115
|
return true;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
// Equal stat tuples do not prove bytes past the hashed prefix. Files outside
|
|
119
|
+
// that budget must not be captured, reused, or inserted.
|
|
120
|
+
function _cacheableStats(statsByAbs) {
|
|
121
|
+
return !!statsByAbs && Object.values(statsByAbs).every((stat) => canCacheReadFile(stat));
|
|
122
|
+
}
|
|
123
|
+
|
|
117
124
|
function _getOrCreate(sessionId) {
|
|
118
125
|
let m = _bySession.get(sessionId);
|
|
119
126
|
if (!m) {
|
|
@@ -157,20 +164,22 @@ export function captureReadCacheState({ args, cwd }) {
|
|
|
157
164
|
const key = _keyFor(args, cwd);
|
|
158
165
|
if (key === null && Array.isArray(args?.path ?? args?.file_path)) {
|
|
159
166
|
const parsed = _arrayKeyAndStats(args, cwd);
|
|
160
|
-
if (!parsed ||
|
|
167
|
+
if (!parsed || !_cacheableStats(parsed.statsByAbs)) return null;
|
|
161
168
|
return { ...parsed, kind: 'array' };
|
|
162
169
|
}
|
|
163
170
|
if (!key) return null;
|
|
164
171
|
const abs = _absFromKey(key);
|
|
165
172
|
const stat = _statTuple(abs);
|
|
166
|
-
|
|
173
|
+
if (!canCacheReadFile(stat)) return null;
|
|
174
|
+
return { key, statsByAbs: { [abs]: stat }, kind: 'scalar' };
|
|
167
175
|
}
|
|
168
176
|
|
|
169
177
|
/**
|
|
170
178
|
* Look up a cached read result for this session+args. Stats the file at
|
|
171
179
|
* lookup time and only returns when the stat tuple still matches. Returns
|
|
172
|
-
* null on miss, on path-form mismatch,
|
|
173
|
-
* the
|
|
180
|
+
* null on miss, on path-form mismatch, on stat failure, or when any file
|
|
181
|
+
* exceeds the prefix budget. On hit returns the full entry
|
|
182
|
+
* { content, firstToolUseId, ts }.
|
|
174
183
|
*/
|
|
175
184
|
export function tryReadCached({ sessionId, args, cwd }) {
|
|
176
185
|
if (!sessionId) return null;
|
|
@@ -182,7 +191,7 @@ export function tryReadCached({ sessionId, args, cwd }) {
|
|
|
182
191
|
if (!map) return null;
|
|
183
192
|
const entry = map.get(parsed.key);
|
|
184
193
|
if (entry?.kind !== 'array') return null;
|
|
185
|
-
if (!_arrayStatsValid(entry.statsByAbs)) {
|
|
194
|
+
if (!_arrayStatsValid(entry.statsByAbs) || !_cacheableStats(entry.statsByAbs)) {
|
|
186
195
|
map.delete(parsed.key);
|
|
187
196
|
_ridxPruneKey(sessionId, parsed.key);
|
|
188
197
|
return null;
|
|
@@ -197,7 +206,7 @@ export function tryReadCached({ sessionId, args, cwd }) {
|
|
|
197
206
|
const entry = map.get(key);
|
|
198
207
|
if (!entry) return null;
|
|
199
208
|
const fresh = _statTuple(_absFromKey(key));
|
|
200
|
-
if (!_statEqual(entry.stat, fresh)) {
|
|
209
|
+
if (!_statEqual(entry.stat, fresh) || !canCacheReadFile(fresh)) {
|
|
201
210
|
map.delete(key);
|
|
202
211
|
_ridxPruneKey(sessionId, key);
|
|
203
212
|
return null;
|
|
@@ -208,7 +217,8 @@ export function tryReadCached({ sessionId, args, cwd }) {
|
|
|
208
217
|
}
|
|
209
218
|
|
|
210
219
|
/**
|
|
211
|
-
* Cache a successful read result. Skip caching if
|
|
220
|
+
* Cache a successful read result. Skip caching if a file is missing or
|
|
221
|
+
* outside the prefix budget.
|
|
212
222
|
* `toolUseId` is the tool_use id of the FIRST call that populated the entry.
|
|
213
223
|
* Async executors pass their pre-execution `readState`; null disables caching
|
|
214
224
|
* when the source version could not be established. Omission retains direct
|
|
@@ -218,7 +228,7 @@ export function setReadCached({ sessionId, args, cwd, content, toolUseId, readSt
|
|
|
218
228
|
if (!sessionId) return;
|
|
219
229
|
if (typeof content !== 'string' || content.length === 0) return;
|
|
220
230
|
const parsed = captureReadCacheState({ args, cwd });
|
|
221
|
-
if (!parsed) return;
|
|
231
|
+
if (!parsed || !_cacheableStats(parsed.statsByAbs)) return;
|
|
222
232
|
if (
|
|
223
233
|
readState !== undefined &&
|
|
224
234
|
(!readState ||
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session catalog DB, phase 2: in the daemon, SQLite is the read source of
|
|
3
|
+
* the ordinary session catalog.
|
|
4
|
+
*
|
|
5
|
+
* Everything runs on one worker thread (./worker.mjs): the catalog engine's
|
|
6
|
+
* change detection (summary index, sessions directory, per-file stamps, `.hb`
|
|
7
|
+
* liveness, background verification) feeds `<dataDir>/session-catalog.sqlite`
|
|
8
|
+
* and an ordinary listing is one request that answers from it. This thread
|
|
9
|
+
* does no filesystem work for it and never loads node:sqlite.
|
|
10
|
+
*
|
|
11
|
+
* JSON stays what every other process reads and writes: the store keeps
|
|
12
|
+
* writing the summary index exactly as before. When one of the daemon's own
|
|
13
|
+
* writes has changed a session file on disk, the store hints the id here
|
|
14
|
+
* (hintSessionCatalogStorage); the worker re-checks it from storage on the
|
|
15
|
+
* next listing. Hints carry no row data. Whenever the DB cannot answer — not
|
|
16
|
+
* the daemon, not Electron, SQLite gate, another process holds the DB,
|
|
17
|
+
* off-switch, backfill or rebuild in progress, any error — `list()` resolves
|
|
18
|
+
* null and the caller uses the JSON path unchanged.
|
|
19
|
+
*/
|
|
20
|
+
import { isMainThread } from 'node:worker_threads';
|
|
21
|
+
import { createWorkerRequestClient } from '../../../../shared/worker-requests.mjs';
|
|
22
|
+
import { dataDir as currentDataDir } from '../store-summary-locations.mjs';
|
|
23
|
+
|
|
24
|
+
export const SESSION_CATALOG_DB_FILE = 'session-catalog.sqlite';
|
|
25
|
+
/** Config: `{ "sessionStore": { "sqliteCatalog": false } }` forces the JSON
|
|
26
|
+
* path (an off-switch); absent or true uses the DB whenever it can. */
|
|
27
|
+
export const SESSION_STORE_CONFIG_SECTION = 'sessionStore';
|
|
28
|
+
export const SESSION_STORE_CONFIG_DEFAULTS = Object.freeze({ sqliteCatalog: true });
|
|
29
|
+
/** Stable prefix of every daemon.log line of the catalog DB. */
|
|
30
|
+
export const SESSION_CATALOG_LOG_PREFIX = '[session-catalog-db]';
|
|
31
|
+
|
|
32
|
+
// A worker that keeps dying is given up on for a while.
|
|
33
|
+
const CRASH_WINDOW_MS = 10 * 60_000;
|
|
34
|
+
const MAX_CRASHES = 3;
|
|
35
|
+
const ERROR_LOG_INTERVAL_MS = 60_000;
|
|
36
|
+
|
|
37
|
+
let testOptions = null;
|
|
38
|
+
let requestWorker = null;
|
|
39
|
+
let controller = null;
|
|
40
|
+
const changeListeners = new Set();
|
|
41
|
+
const noop = () => {};
|
|
42
|
+
|
|
43
|
+
function log(line) {
|
|
44
|
+
const text = `${SESSION_CATALOG_LOG_PREFIX} ${line}`;
|
|
45
|
+
if (typeof testOptions?.log === 'function') {
|
|
46
|
+
testOptions.log(text);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// The daemon's stderr is daemon.log (mirrored by the spawner before ready,
|
|
50
|
+
// redirected into the file after).
|
|
51
|
+
try {
|
|
52
|
+
process.stderr.write(`${text}\n`);
|
|
53
|
+
} catch {
|
|
54
|
+
/* logging is best effort */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onWorkerEvent(event) {
|
|
59
|
+
if (event?.type === 'log') log(event.line);
|
|
60
|
+
else if (event?.type === 'changed') {
|
|
61
|
+
for (const listener of changeListeners) {
|
|
62
|
+
try {
|
|
63
|
+
listener();
|
|
64
|
+
} catch {
|
|
65
|
+
/* a listener's failure is its own */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function request(payload) {
|
|
72
|
+
requestWorker ||= createWorkerRequestClient(new URL('./worker.mjs', import.meta.url), {
|
|
73
|
+
// The worker is the only node:sqlite user; its experimental warning is noise.
|
|
74
|
+
execArgv: ['--disable-warning=ExperimentalWarning'],
|
|
75
|
+
onEvent: onWorkerEvent,
|
|
76
|
+
});
|
|
77
|
+
return requestWorker(payload);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Worker settings a test configured (functions stay on this thread). */
|
|
81
|
+
function workerSettings() {
|
|
82
|
+
if (!testOptions) return undefined;
|
|
83
|
+
const settings = {};
|
|
84
|
+
for (const [key, value] of Object.entries(testOptions)) {
|
|
85
|
+
if (typeof value !== 'function' && key !== 'writer') settings[key] = value;
|
|
86
|
+
}
|
|
87
|
+
return settings;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* True only in the daemon process itself, under the packaged or dev Electron
|
|
92
|
+
* runtime: daemon.mjs sets MIXDOG_DAEMON_HOST and MIXDOG_SERVER_PID (its own
|
|
93
|
+
* pid) at startup; children inherit the environment but have another pid,
|
|
94
|
+
* worker threads are not the main thread, and a daemon started by the CLI
|
|
95
|
+
* under system Node stays on JSON.
|
|
96
|
+
*/
|
|
97
|
+
export function isSessionCatalogDbWriter() {
|
|
98
|
+
if (testOptions && 'writer' in testOptions) return testOptions.writer === true;
|
|
99
|
+
return (
|
|
100
|
+
isMainThread &&
|
|
101
|
+
Boolean(process.versions.electron) &&
|
|
102
|
+
process.env.MIXDOG_DAEMON_HOST === '1' &&
|
|
103
|
+
process.env.MIXDOG_SERVER_PID === String(process.pid)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The catalog DB of `dataDir`, or null when this process does not keep one. */
|
|
108
|
+
export function sessionCatalogDb(dataDir) {
|
|
109
|
+
if (!isSessionCatalogDbWriter()) return null;
|
|
110
|
+
if (controller?.dataDir === dataDir) return controller;
|
|
111
|
+
void controller?.close();
|
|
112
|
+
controller = new SessionCatalogDb(dataDir);
|
|
113
|
+
return controller;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The existing catalog DB of `dataDir` (never creates one). */
|
|
117
|
+
export function currentSessionCatalogDb(dataDir) {
|
|
118
|
+
return controller?.dataDir === dataDir ? controller : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `listener()` runs when the worker's background pass finds a changed
|
|
122
|
+
* session file (an ordinary listing would now differ). */
|
|
123
|
+
export function onSessionCatalogDbChange(listener) {
|
|
124
|
+
changeListeners.add(listener);
|
|
125
|
+
return () => changeListeners.delete(listener);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Storage hint from the store (store/summary-cache.mjs): a write of this
|
|
130
|
+
* process changed the files of `ids` — a save landed, a delete removed the
|
|
131
|
+
* file. Tagged with the current data dir; the worker re-checks those ids from
|
|
132
|
+
* storage on the next listing. A lost hint only delays the change to the
|
|
133
|
+
* engine's own detection.
|
|
134
|
+
*/
|
|
135
|
+
export function hintSessionCatalogStorage(ids) {
|
|
136
|
+
currentSessionCatalogDb(currentDataDir())?.hint(ids);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Closes the catalog DB (releasing its file, WAL and lock) — the teardown
|
|
140
|
+
* boundary before a data dir is removed or switched. */
|
|
141
|
+
export async function closeSessionCatalogDb() {
|
|
142
|
+
const current = controller;
|
|
143
|
+
controller = null;
|
|
144
|
+
if (current) await current.close();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
class SessionCatalogDb {
|
|
148
|
+
constructor(dataDir) {
|
|
149
|
+
this.dataDir = dataDir;
|
|
150
|
+
this.state = 'active'; // active | off (final: gate) | failed (until failedUntil) | closed
|
|
151
|
+
this.failedUntil = 0;
|
|
152
|
+
this.hints = new Set();
|
|
153
|
+
this.flushHandle = null;
|
|
154
|
+
this.started = false;
|
|
155
|
+
this.crashes = [];
|
|
156
|
+
this.errorLoggedAt = 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
send(op, payload = {}) {
|
|
160
|
+
// No running worker: this request starts one. After the first start it
|
|
161
|
+
// is a respawn (the last one died): it reopens and backfills by itself.
|
|
162
|
+
if (!requestWorker?.running()) {
|
|
163
|
+
if (this.started) this.noteRespawn();
|
|
164
|
+
this.started = true;
|
|
165
|
+
}
|
|
166
|
+
return request({ op, dataDir: this.dataDir, settings: workerSettings(), ...payload }).catch((error) => {
|
|
167
|
+
this.noteError(error);
|
|
168
|
+
throw error;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A worker died; more than MAX_CRASHES deaths in the window give up. */
|
|
173
|
+
noteRespawn() {
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
this.crashes = this.crashes.filter((at) => now - at < CRASH_WINDOW_MS);
|
|
176
|
+
this.crashes.push(now);
|
|
177
|
+
if (this.crashes.length > MAX_CRASHES && this.state === 'active') {
|
|
178
|
+
this.state = 'failed';
|
|
179
|
+
this.failedUntil = now + CRASH_WINDOW_MS;
|
|
180
|
+
log(`worker failed ${this.crashes.length} times in 10 min; catalog stays JSON`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
noteError(error) {
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
if (now - this.errorLoggedAt >= ERROR_LOG_INTERVAL_MS) {
|
|
187
|
+
this.errorLoggedAt = now;
|
|
188
|
+
log(`request failed: ${error?.message || error}; listing answered from JSON`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
usable() {
|
|
193
|
+
if (this.state === 'failed' && Date.now() >= this.failedUntil) {
|
|
194
|
+
this.state = 'active';
|
|
195
|
+
this.crashes = [];
|
|
196
|
+
}
|
|
197
|
+
return this.state === 'active';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
hint(ids) {
|
|
201
|
+
if (this.state !== 'active') return;
|
|
202
|
+
for (const id of ids) if (id) this.hints.add(String(id));
|
|
203
|
+
if (this.hints.size === 0) return;
|
|
204
|
+
this.flushHandle ||= setImmediate(() => {
|
|
205
|
+
this.flushHandle = null;
|
|
206
|
+
this.flushHints();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Posts the collected hints ahead of any later request. A worker that is
|
|
211
|
+
* not running gets none: a (re)started worker rescans storage anyway. */
|
|
212
|
+
flushHints() {
|
|
213
|
+
if (this.hints.size === 0) return;
|
|
214
|
+
const ids = [...this.hints];
|
|
215
|
+
this.hints.clear();
|
|
216
|
+
if (!requestWorker?.running()) return;
|
|
217
|
+
this.send('hint', { ids }).catch(noop);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* One ordinary listing (`page` = `{ limit, after }` or null for all):
|
|
222
|
+
* `{ rows, more }`, or null when the DB cannot answer (use JSON). Hints
|
|
223
|
+
* collected before this call are posted first.
|
|
224
|
+
*/
|
|
225
|
+
async list(page = null) {
|
|
226
|
+
if (!this.usable()) return null;
|
|
227
|
+
this.flushHints();
|
|
228
|
+
let answer;
|
|
229
|
+
try {
|
|
230
|
+
answer = await this.send('list', { page });
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
if (answer.status === 'ok') return { rows: answer.rows, more: answer.more };
|
|
235
|
+
if (answer.final && this.state === 'active') this.state = 'off';
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
verifyFiles() {
|
|
240
|
+
if (!this.usable()) return Promise.resolve();
|
|
241
|
+
return this.send('verifyFiles').catch(noop);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Test/benchmark seams. */
|
|
245
|
+
status() {
|
|
246
|
+
return this.send('status');
|
|
247
|
+
}
|
|
248
|
+
shadow() {
|
|
249
|
+
return this.send('shadow');
|
|
250
|
+
}
|
|
251
|
+
settled() {
|
|
252
|
+
this.flushHints();
|
|
253
|
+
return this.send('settle');
|
|
254
|
+
}
|
|
255
|
+
testExec(sql, params = []) {
|
|
256
|
+
return this.send('testExec', { sql, params });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async close() {
|
|
260
|
+
if (this.state === 'closed') return;
|
|
261
|
+
this.state = 'closed';
|
|
262
|
+
if (this.flushHandle) clearImmediate(this.flushHandle);
|
|
263
|
+
this.flushHandle = null;
|
|
264
|
+
this.hints.clear();
|
|
265
|
+
// Only a running worker can hold the DB open.
|
|
266
|
+
if (requestWorker?.running()) {
|
|
267
|
+
await request({ op: 'close', dataDir: this.dataDir }).catch(noop);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Test seams ──────────────────────────────────────────────────────────────
|
|
273
|
+
/** Test seam: `{ writer, enabled, sqliteVersion, backfillBatch,
|
|
274
|
+
* shadowIntervalMs, retryMs, fenceTimeoutMs, crashAfterBatches, log }`;
|
|
275
|
+
* null restores defaults. */
|
|
276
|
+
export function configureSessionCatalogDbForTest(options) {
|
|
277
|
+
testOptions = options ? { ...options } : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Test seam: post a storage hint tagged with `dataDir` (another data dir's
|
|
281
|
+
* hint must be ignored by the worker). */
|
|
282
|
+
export function postSessionCatalogHintForTest(dataDir, ids) {
|
|
283
|
+
return request({ op: 'hint', dataDir, ids });
|
|
284
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session catalog DB: table layout, row codec and the listing comparison.
|
|
3
|
+
* Pure (no IO, no node:sqlite) so both the worker and tests can use it.
|
|
4
|
+
*
|
|
5
|
+
* One row per catalog session: every field store-summary-reader.mjs
|
|
6
|
+
* normalizedRow produces, the storage fingerprint (full stamp of the session
|
|
7
|
+
* file the row was judged at), the title normalized at write, and the
|
|
8
|
+
* bookkeeping the mirror needs (lead visibility, sync epoch, row signature).
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
/** Bumped whenever the table layout or a codec changes: the DB is then
|
|
13
|
+
* dropped and backfilled again from the JSON catalog. */
|
|
14
|
+
export const SCHEMA_VERSION = 3;
|
|
15
|
+
/** PRAGMA application_id ('MXSC'): a file without it is not this catalog. */
|
|
16
|
+
export const APPLICATION_ID = 0x4d585343;
|
|
17
|
+
|
|
18
|
+
// [row key, column, codec]
|
|
19
|
+
// real/int numbers; bool 0/1; text strings
|
|
20
|
+
// loose whatever the source carried (string/number/null as is, anything
|
|
21
|
+
// else as a JSON BLOB), so the row round-trips exactly
|
|
22
|
+
// json object or null, as JSON text
|
|
23
|
+
// optjson like json, but the key is absent from the row when NULL
|
|
24
|
+
const FIELDS = [
|
|
25
|
+
['id', 'id', 'text'],
|
|
26
|
+
['updatedAt', 'updated_at', 'real'],
|
|
27
|
+
['lastUsedAt', 'last_used_at', 'real'],
|
|
28
|
+
['createdAt', 'created_at', 'real'],
|
|
29
|
+
['lastHeartbeatAt', 'last_heartbeat_at', 'real'],
|
|
30
|
+
// Always 0 in a stored row: liveness is overlaid from `.hb` sidecars at read.
|
|
31
|
+
['heartbeatAt', 'heartbeat_at', 'real'],
|
|
32
|
+
['closed', 'closed', 'bool'],
|
|
33
|
+
['status', 'status', 'text'],
|
|
34
|
+
['owner', 'owner', 'loose'],
|
|
35
|
+
['agent', 'agent', 'loose'],
|
|
36
|
+
['sourceType', 'source_type', 'loose'],
|
|
37
|
+
['sourceName', 'source_name', 'loose'],
|
|
38
|
+
['scopeKey', 'scope_key', 'loose'],
|
|
39
|
+
['ownerSessionId', 'owner_session_id', 'loose'],
|
|
40
|
+
['visibility', 'visibility', 'loose'],
|
|
41
|
+
['clientHostPid', 'client_host_pid', 'loose'],
|
|
42
|
+
['cwd', 'cwd', 'loose'],
|
|
43
|
+
['desktopSession', 'desktop_session', 'json'],
|
|
44
|
+
['workspace', 'workspace', 'optjson'],
|
|
45
|
+
['provider', 'provider', 'loose'],
|
|
46
|
+
['model', 'model', 'loose'],
|
|
47
|
+
['agentTag', 'agent_tag', 'loose'],
|
|
48
|
+
['task_id', 'task_id', 'loose'],
|
|
49
|
+
['permission', 'permission', 'loose'],
|
|
50
|
+
['toolPermission', 'tool_permission', 'loose'],
|
|
51
|
+
['messageCount', 'message_count', 'int'],
|
|
52
|
+
['title', 'title', 'text'],
|
|
53
|
+
['preview', 'preview', 'text'],
|
|
54
|
+
['generation', 'generation', 'loose'],
|
|
55
|
+
['storageMtimeMs', 'storage_mtime_ms', 'real'],
|
|
56
|
+
['storageSize', 'storage_size', 'real'],
|
|
57
|
+
['detachedReason', 'detached_reason', 'loose'],
|
|
58
|
+
// importOrigin.key of an imported conversation (schema 3).
|
|
59
|
+
['importKey', 'import_key', 'loose'],
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const COLUMN_TYPES = { text: 'TEXT', real: 'REAL', int: 'INTEGER', bool: 'INTEGER', json: 'TEXT', optjson: 'TEXT' };
|
|
63
|
+
|
|
64
|
+
const columnDefinition = ([, column, codec]) => {
|
|
65
|
+
if (column === 'id') return 'id TEXT PRIMARY KEY';
|
|
66
|
+
// No declared type = no affinity: loose values are stored exactly as bound.
|
|
67
|
+
return codec === 'loose' ? column : `${column} ${COLUMN_TYPES[codec]}`;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const SCHEMA_SQL = `
|
|
71
|
+
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
|
72
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
73
|
+
${FIELDS.map(columnDefinition).join(',\n ')},
|
|
74
|
+
activity REAL NOT NULL,
|
|
75
|
+
title_norm TEXT NOT NULL,
|
|
76
|
+
fingerprint TEXT,
|
|
77
|
+
lead_visible INTEGER NOT NULL,
|
|
78
|
+
row_sig TEXT NOT NULL
|
|
79
|
+
);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS sessions_recent ON sessions(activity DESC, id);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS sessions_lead_recent ON sessions(lead_visible, activity DESC, id);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS sessions_visibility_recent ON sessions(visibility, activity DESC, id);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS sessions_owner_session ON sessions(owner_session_id);
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
const WRITE_COLUMNS = [
|
|
87
|
+
...FIELDS.map(([, column]) => column),
|
|
88
|
+
'activity',
|
|
89
|
+
'title_norm',
|
|
90
|
+
'fingerprint',
|
|
91
|
+
'lead_visible',
|
|
92
|
+
'row_sig',
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
export const UPSERT_SQL = `INSERT INTO sessions(${WRITE_COLUMNS.join(',')})
|
|
96
|
+
VALUES (${WRITE_COLUMNS.map(() => '?').join(',')})
|
|
97
|
+
ON CONFLICT(id) DO UPDATE SET ${WRITE_COLUMNS.slice(1)
|
|
98
|
+
.map((column) => `${column}=excluded.${column}`)
|
|
99
|
+
.join(',')}`;
|
|
100
|
+
|
|
101
|
+
/** Recent-activity key of the JSON listing (store-summary-reader byRecentActivity). */
|
|
102
|
+
export function activityOf(row) {
|
|
103
|
+
return row.lastUsedAt || row.updatedAt || 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Newest activity first, ties by id: the DB order and the keyset order.
|
|
107
|
+
* Ids are ASCII ([A-Za-z0-9_-]), so code-unit order equals SQLite BINARY. */
|
|
108
|
+
export function byActivityThenId(left, right) {
|
|
109
|
+
const delta = activityOf(right) - activityOf(left);
|
|
110
|
+
if (delta !== 0) return delta;
|
|
111
|
+
if (left.id === right.id) return 0;
|
|
112
|
+
return left.id < right.id ? -1 : 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Title normalized once at write: what a title lookup or sort compares. */
|
|
116
|
+
export function normalizedTitle(row) {
|
|
117
|
+
return String(row.title || row.preview || '')
|
|
118
|
+
.normalize('NFKC')
|
|
119
|
+
.toLowerCase()
|
|
120
|
+
.replace(/\s+/g, ' ')
|
|
121
|
+
.trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function encodeLoose(value) {
|
|
125
|
+
if (value === null || value === undefined) return null;
|
|
126
|
+
if (typeof value === 'string' || typeof value === 'number') return value;
|
|
127
|
+
return Buffer.from(JSON.stringify(value), 'utf8');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function decodeLoose(value) {
|
|
131
|
+
if (value instanceof Uint8Array) return JSON.parse(Buffer.from(value).toString('utf8'));
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function encodeField(codec, value) {
|
|
136
|
+
switch (codec) {
|
|
137
|
+
case 'bool':
|
|
138
|
+
return value === true ? 1 : 0;
|
|
139
|
+
case 'json':
|
|
140
|
+
case 'optjson':
|
|
141
|
+
return value == null ? null : JSON.stringify(value);
|
|
142
|
+
case 'loose':
|
|
143
|
+
return encodeLoose(value);
|
|
144
|
+
default:
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function decodeField(codec, value) {
|
|
150
|
+
switch (codec) {
|
|
151
|
+
case 'bool':
|
|
152
|
+
return value === 1;
|
|
153
|
+
case 'json':
|
|
154
|
+
case 'optjson':
|
|
155
|
+
return value == null ? null : JSON.parse(value);
|
|
156
|
+
case 'loose':
|
|
157
|
+
return decodeLoose(value);
|
|
158
|
+
default:
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Signature of what one mirror entry writes: equal signature, no write. */
|
|
164
|
+
export function rowSignature({ row, fingerprint, leadVisible }) {
|
|
165
|
+
return createHash('sha1')
|
|
166
|
+
.update(JSON.stringify([fingerprint ?? null, leadVisible === true, row]))
|
|
167
|
+
.digest('base64');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Bind parameters of UPSERT_SQL for one mirror entry. */
|
|
171
|
+
export function upsertParams(entry, signature) {
|
|
172
|
+
const { row } = entry;
|
|
173
|
+
return [
|
|
174
|
+
...FIELDS.map(([key, , codec]) => encodeField(codec, row[key])),
|
|
175
|
+
activityOf(row),
|
|
176
|
+
normalizedTitle(row),
|
|
177
|
+
entry.fingerprint ?? null,
|
|
178
|
+
entry.leadVisible === true ? 1 : 0,
|
|
179
|
+
signature,
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A `sessions` row back in normalizedRow shape. */
|
|
184
|
+
export function rowFromDb(record) {
|
|
185
|
+
const row = {};
|
|
186
|
+
for (const [key, column, codec] of FIELDS) {
|
|
187
|
+
const value = decodeField(codec, record[column]);
|
|
188
|
+
if (codec === 'optjson' && value === null) continue;
|
|
189
|
+
row[key] = value;
|
|
190
|
+
}
|
|
191
|
+
return row;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Overlays a listing adds at read time; never part of the comparison.
|
|
195
|
+
const OVERLAY_KEYS = new Set(['heartbeatAt', 'agentHeartbeatAt']);
|
|
196
|
+
|
|
197
|
+
/** Comparable JSON of a row's stored fields (overlays excluded). */
|
|
198
|
+
function comparable(row) {
|
|
199
|
+
const out = [];
|
|
200
|
+
for (const [key] of FIELDS) {
|
|
201
|
+
if (OVERLAY_KEYS.has(key)) continue;
|
|
202
|
+
out.push(row[key] === undefined ? null : row[key]);
|
|
203
|
+
}
|
|
204
|
+
return JSON.stringify(out);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const FIRST_IDS = 5;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Shadow comparison of the DB listing against the JSON listing, both in the
|
|
211
|
+
* one catalog order (byActivityThenId) and compared exactly as given: ids
|
|
212
|
+
* present in only one, positions of the common ids, and stored row fields.
|
|
213
|
+
*/
|
|
214
|
+
export function compareListings(json, dbRows) {
|
|
215
|
+
const jsonById = new Map(json.map((row) => [row.id, row]));
|
|
216
|
+
const dbById = new Map(dbRows.map((row) => [row.id, row]));
|
|
217
|
+
const missing = json.filter((row) => !dbById.has(row.id)).map((row) => row.id);
|
|
218
|
+
const extra = dbRows.filter((row) => !jsonById.has(row.id)).map((row) => row.id);
|
|
219
|
+
const commonJson = json.filter((row) => dbById.has(row.id));
|
|
220
|
+
const commonDb = dbRows.filter((row) => jsonById.has(row.id));
|
|
221
|
+
const order = [];
|
|
222
|
+
for (let index = 0; index < commonJson.length; index++) {
|
|
223
|
+
if (commonJson[index].id !== commonDb[index].id) order.push(commonJson[index].id);
|
|
224
|
+
}
|
|
225
|
+
const fields = commonJson.filter((row) => comparable(row) !== comparable(dbById.get(row.id))).map((row) => row.id);
|
|
226
|
+
return {
|
|
227
|
+
json: json.length,
|
|
228
|
+
db: dbRows.length,
|
|
229
|
+
missing: missing.length,
|
|
230
|
+
extra: extra.length,
|
|
231
|
+
order: order.length,
|
|
232
|
+
fields: fields.length,
|
|
233
|
+
ids: {
|
|
234
|
+
missing: missing.slice(0, FIRST_IDS),
|
|
235
|
+
extra: extra.slice(0, FIRST_IDS),
|
|
236
|
+
order: order.slice(0, FIRST_IDS),
|
|
237
|
+
fields: fields.slice(0, FIRST_IDS),
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Keyset cursor (activity time, id) ──────────────────────────────────────
|
|
243
|
+
/** Opaque cursor naming the last row of a page. */
|
|
244
|
+
export function encodeCatalogCursor(row) {
|
|
245
|
+
return Buffer.from(JSON.stringify([activityOf(row), row.id]), 'utf8').toString('base64url');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** `{ activity, id }` of a cursor, null for none; throws on a malformed one. */
|
|
249
|
+
export function decodeCatalogCursor(cursor) {
|
|
250
|
+
if (cursor === undefined || cursor === null || cursor === '') return null;
|
|
251
|
+
let parsed;
|
|
252
|
+
try {
|
|
253
|
+
parsed = JSON.parse(Buffer.from(String(cursor), 'base64url').toString('utf8'));
|
|
254
|
+
} catch {
|
|
255
|
+
parsed = null;
|
|
256
|
+
}
|
|
257
|
+
if (!Array.isArray(parsed) || !Number.isFinite(parsed[0]) || typeof parsed[1] !== 'string') {
|
|
258
|
+
throw Object.assign(new Error('invalid session catalog cursor'), { code: 'EINVALIDCURSOR' });
|
|
259
|
+
}
|
|
260
|
+
return { activity: parsed[0], id: parsed[1] };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Whether `row` sorts strictly after the cursor position. */
|
|
264
|
+
export function isAfterCursor(row, after) {
|
|
265
|
+
if (!after) return true;
|
|
266
|
+
const activity = activityOf(row);
|
|
267
|
+
return activity < after.activity || (activity === after.activity && row.id > after.id);
|
|
268
|
+
}
|