mixdog 0.9.3 → 0.9.5
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 +112 -38
- package/package.json +10 -3
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/r5-orchestrated-task.json +7 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r13-clientmeta.json +464 -0
- package/scripts/bench/round-r14-betafeatures.json +466 -0
- package/scripts/bench/round-r15-fulldefault.json +462 -0
- package/scripts/bench/round-r16-sessionid.json +466 -0
- package/scripts/bench/round-r17-wirebytes.json +456 -0
- package/scripts/bench/round-r18-prewarm.json +468 -0
- package/scripts/bench/round-r19-clean.json +472 -0
- package/scripts/bench/round-r20-prewarm-clean.json +475 -0
- package/scripts/bench/round-r21-delta-retry.json +473 -0
- package/scripts/bench/round-r22-full-probe.json +693 -0
- package/scripts/bench/round-r23-itemprobe.json +701 -0
- package/scripts/bench/round-r24-shapefix.json +677 -0
- package/scripts/bench/round-r25-serial.json +464 -0
- package/scripts/bench/round-r26-parallel3.json +671 -0
- package/scripts/bench/round-r27-parallel10.json +894 -0
- package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
- package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
- package/scripts/bench/round-r30-instid.json +253 -0
- package/scripts/bench/round-r31-upgradeprobe.json +256 -0
- package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
- package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
- package/scripts/bench/round-r34-orchestrated.json +120 -0
- package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
- package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +251 -32
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +3 -4
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/model-catalog-audit.mjs +209 -0
- package/scripts/model-list-sanitize-test.mjs +37 -0
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-bench.mjs +76 -13
- package/scripts/recall-quality-cases.json +12 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +25 -65
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +4 -1
- package/src/agents/heavy-worker/AGENT.md +9 -8
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/scheduler-task/AGENT.md +2 -3
- package/src/agents/webhook-handler/AGENT.md +2 -3
- package/src/agents/worker/AGENT.md +10 -7
- package/src/app.mjs +12 -1
- package/src/headless-role.mjs +7 -1
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +647 -2056
- package/src/output-styles/default.md +30 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +8 -6
- package/src/output-styles/simple.md +21 -7
- package/src/rules/agent/00-common.md +6 -3
- package/src/rules/agent/30-explorer.md +16 -5
- package/src/rules/lead/01-general.md +5 -5
- package/src/rules/lead/lead-brief.md +15 -0
- package/src/rules/lead/lead-tool.md +6 -15
- package/src/rules/shared/01-tool.md +17 -21
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +27 -318
- package/src/runtime/channels/backends/telegram.mjs +8 -12
- package/src/runtime/channels/index.mjs +247 -701
- package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
- package/src/runtime/channels/lib/config.mjs +37 -149
- package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
- package/src/runtime/channels/lib/event-queue.mjs +78 -13
- package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
- package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/scheduler.mjs +27 -113
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +43 -616
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +210 -1948
- package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/ko-morph.mjs +195 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
- package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +802 -0
- package/src/runtime/memory/lib/recall-format.mjs +55 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +5 -13
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/config.mjs +14 -4
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
- package/src/runtime/shared/schedules-store.mjs +13 -3
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +119 -2
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +11 -9
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +205 -0
- package/src/session-runtime/quick-search-models.mjs +47 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +352 -0
- package/src/session-runtime/tool-catalog.mjs +29 -29
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/session-runtime/workflow.mjs +1 -1
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +138 -669
- package/src/standalone/channel-admin.mjs +102 -90
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +64 -14
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +77 -870
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +5 -1
- package/src/standalone/provider-admin.mjs +1 -16
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +1059 -8110
- package/src/tui/app/app-format.mjs +213 -0
- package/src/tui/app/channel-pickers.mjs +508 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +356 -0
- package/src/tui/app/model-options.mjs +334 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +489 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +677 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +512 -0
- package/src/tui/app/use-transcript-window.mjs +607 -0
- package/src/tui/components/ConfirmBar.jsx +10 -7
- package/src/tui/components/Picker.jsx +64 -15
- package/src/tui/components/PromptInput.jsx +33 -102
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +69 -15
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +52 -594
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +13553 -12384
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine.mjs +170 -574
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +65 -1
- package/src/tui/input-editing.mjs +2 -2
- package/src/tui/markdown/format-token.mjs +4 -1
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +77 -501
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +16 -18
- package/src/workflows/sequential/WORKFLOW.md +16 -18
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +112 -7
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/channels/lib/holidays.mjs +0 -138
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/input-editing.selection.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/paste-fix.test.mjs +0 -119
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -28,8 +28,8 @@ import {
|
|
|
28
28
|
readMarkdownDocument,
|
|
29
29
|
} from './runtime/shared/markdown-frontmatter.mjs';
|
|
30
30
|
import { setConfiguredShell } from './runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs';
|
|
31
|
+
import { hasUserConversationMessage } from './runtime/agent/orchestrator/session/manager/prompt-utils.mjs';
|
|
31
32
|
import {
|
|
32
|
-
PROVIDER_STATUS_TOOL,
|
|
33
33
|
beginOAuthProviderLogin,
|
|
34
34
|
forgetProviderAuth,
|
|
35
35
|
isKnownProvider,
|
|
@@ -54,14 +54,12 @@ import {
|
|
|
54
54
|
} from './runtime/agent/orchestrator/providers/custom-tool-wire.mjs';
|
|
55
55
|
import {
|
|
56
56
|
channelSetup,
|
|
57
|
-
deleteChannel,
|
|
58
57
|
deleteSchedule,
|
|
59
58
|
deleteWebhook,
|
|
60
59
|
forgetDiscordToken,
|
|
61
60
|
forgetTelegramToken,
|
|
62
61
|
forgetWebhookAuthtoken,
|
|
63
|
-
|
|
64
|
-
saveChannel,
|
|
62
|
+
setChannel,
|
|
65
63
|
saveDiscordToken,
|
|
66
64
|
saveTelegramToken,
|
|
67
65
|
saveSchedule,
|
|
@@ -137,9 +135,14 @@ import {
|
|
|
137
135
|
normalizeSystemShellConfig,
|
|
138
136
|
normalizeSystemShellCommand,
|
|
139
137
|
normalizeAutoClearConfig,
|
|
138
|
+
resolveAutoClearIdleMs,
|
|
139
|
+
autoClearIdleMsForProvider,
|
|
140
|
+
autoClearProviderDefaults,
|
|
140
141
|
normalizeCompactionConfig,
|
|
141
142
|
moduleEnabled,
|
|
142
143
|
setModuleEnabledInConfig,
|
|
144
|
+
recapEnabled,
|
|
145
|
+
setRecapEnabledInConfig,
|
|
143
146
|
formatDurationMs,
|
|
144
147
|
parseDurationMs,
|
|
145
148
|
modelMetaLooksResolved,
|
|
@@ -174,6 +177,7 @@ import {
|
|
|
174
177
|
agentPresetSlot,
|
|
175
178
|
normalizeAgentId,
|
|
176
179
|
normalizeWorkflowId,
|
|
180
|
+
DEFAULT_WORKFLOW_ID,
|
|
177
181
|
createWorkflowHelpers,
|
|
178
182
|
normalizeSearchProviderId,
|
|
179
183
|
isDefaultSearchRouteConfig,
|
|
@@ -208,6 +212,36 @@ import {
|
|
|
208
212
|
// Re-exported for external consumers (scripts/tool-smoke.mjs) that imported
|
|
209
213
|
// these from this module before the tool-catalog extraction.
|
|
210
214
|
export { defaultDeferredToolNames, compactToolSearchDescription } from './session-runtime/tool-catalog.mjs';
|
|
215
|
+
import {
|
|
216
|
+
TOOL_SEARCH_TOOL,
|
|
217
|
+
CWD_TOOL,
|
|
218
|
+
SKILL_TOOL,
|
|
219
|
+
LEAD_DISALLOWED_TOOLS,
|
|
220
|
+
applyStandaloneToolDefaults,
|
|
221
|
+
} from './session-runtime/tool-defs.mjs';
|
|
222
|
+
import { ONBOARDING_VERSION, QUICK_SEARCH_MODELS } from './session-runtime/quick-search-models.mjs';
|
|
223
|
+
import {
|
|
224
|
+
sortProviderModels as sortProviderModelsRaw,
|
|
225
|
+
providerModelCacheRow as providerModelCacheRowRaw,
|
|
226
|
+
} from './session-runtime/model-recency.mjs';
|
|
227
|
+
import { createNativeSearch } from './session-runtime/native-search.mjs';
|
|
228
|
+
import { createConfigLifecycle } from './session-runtime/config-lifecycle.mjs';
|
|
229
|
+
import { attachSessionHooks } from './session-runtime/session-hooks.mjs';
|
|
230
|
+
import { createQuickModelRows } from './session-runtime/quick-model-rows.mjs';
|
|
231
|
+
import { createWarmupSchedulers } from './session-runtime/warmup-schedulers.mjs';
|
|
232
|
+
import { createPrewarmSchedulers } from './session-runtime/prewarm.mjs';
|
|
233
|
+
import { createMcpGlue } from './session-runtime/mcp-glue.mjs';
|
|
234
|
+
import { createCwdPlugins } from './session-runtime/cwd-plugins.mjs';
|
|
235
|
+
import { createSettingsApi } from './session-runtime/settings-api.mjs';
|
|
236
|
+
import { createProviderModels } from './session-runtime/provider-models.mjs';
|
|
237
|
+
import { createProviderUsage } from './session-runtime/provider-usage.mjs';
|
|
238
|
+
// Re-exported for external consumers (scripts/tool-smoke.mjs) that imported
|
|
239
|
+
// these from this module before the tool-defs extraction.
|
|
240
|
+
export { TOOL_SEARCH_TOOL, SKILL_TOOL };
|
|
241
|
+
// Back-compat test alias; delegates to the extracted helper.
|
|
242
|
+
export function __applyStandaloneToolDefaultsForTest(tool) {
|
|
243
|
+
return applyStandaloneToolDefaults(tool);
|
|
244
|
+
}
|
|
211
245
|
|
|
212
246
|
const RUNTIME = './runtime/agent/orchestrator';
|
|
213
247
|
const SEARCH_RUNTIME = './runtime/search/index.mjs';
|
|
@@ -276,146 +310,7 @@ async function profiledImport(label, spec, { optional = false } = {}) {
|
|
|
276
310
|
throw error;
|
|
277
311
|
}
|
|
278
312
|
}
|
|
279
|
-
export const TOOL_SEARCH_TOOL = {
|
|
280
|
-
name: 'tool_search',
|
|
281
|
-
title: 'Tool Search',
|
|
282
|
-
annotations: {
|
|
283
|
-
title: 'Tool Search',
|
|
284
|
-
readOnlyHint: true,
|
|
285
|
-
destructiveHint: false,
|
|
286
|
-
idempotentHint: true,
|
|
287
|
-
openWorldHint: false,
|
|
288
|
-
agentHidden: true,
|
|
289
|
-
},
|
|
290
|
-
description: 'Search deferred tools; confident queries load matches.',
|
|
291
|
-
inputSchema: {
|
|
292
|
-
type: 'object',
|
|
293
|
-
properties: {
|
|
294
|
-
query: { type: 'string', description: 'Search text; confident hits load. select:a,b forces exact names.' },
|
|
295
|
-
select: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Force exact tool names.' },
|
|
296
|
-
limit: { type: 'number', description: 'Max matches.' },
|
|
297
|
-
},
|
|
298
|
-
additionalProperties: false,
|
|
299
|
-
},
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
const CHANNEL_STATUS_TOOL = {
|
|
303
|
-
name: 'channel_status',
|
|
304
|
-
title: 'Channel Status',
|
|
305
|
-
annotations: {
|
|
306
|
-
title: 'Channel Status',
|
|
307
|
-
readOnlyHint: true,
|
|
308
|
-
destructiveHint: false,
|
|
309
|
-
idempotentHint: true,
|
|
310
|
-
openWorldHint: false,
|
|
311
|
-
agentHidden: true,
|
|
312
|
-
},
|
|
313
|
-
description: 'List channel/schedule/webhook status. No secrets.',
|
|
314
|
-
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const CWD_TOOL = {
|
|
318
|
-
name: 'cwd',
|
|
319
|
-
title: 'Work Project',
|
|
320
|
-
annotations: {
|
|
321
|
-
title: 'Work Project',
|
|
322
|
-
readOnlyHint: false,
|
|
323
|
-
destructiveHint: false,
|
|
324
|
-
idempotentHint: false,
|
|
325
|
-
openWorldHint: false,
|
|
326
|
-
agentHidden: true,
|
|
327
|
-
},
|
|
328
|
-
description: 'Show or set the session work project for tool execution.',
|
|
329
|
-
inputSchema: {
|
|
330
|
-
type: 'object',
|
|
331
|
-
properties: {
|
|
332
|
-
action: { type: 'string', enum: ['get', 'set'], description: 'Default get.' },
|
|
333
|
-
path: { type: 'string', description: 'Project directory for set.' },
|
|
334
|
-
},
|
|
335
|
-
additionalProperties: false,
|
|
336
|
-
},
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
export const SKILL_TOOL = {
|
|
340
|
-
name: 'Skill',
|
|
341
|
-
title: 'Skill',
|
|
342
|
-
annotations: {
|
|
343
|
-
title: 'Skill',
|
|
344
|
-
readOnlyHint: true,
|
|
345
|
-
destructiveHint: false,
|
|
346
|
-
idempotentHint: true,
|
|
347
|
-
openWorldHint: false,
|
|
348
|
-
agentHidden: false,
|
|
349
|
-
},
|
|
350
|
-
description: 'Load a named SKILL.md into context.',
|
|
351
|
-
inputSchema: {
|
|
352
|
-
type: 'object',
|
|
353
|
-
properties: {
|
|
354
|
-
name: { type: 'string', description: 'Skill name.' },
|
|
355
|
-
},
|
|
356
|
-
required: ['name'],
|
|
357
|
-
additionalProperties: false,
|
|
358
|
-
},
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
const LEAD_DISALLOWED_TOOLS = Object.freeze(['diagnostics', 'open_config']);
|
|
362
|
-
const AGENT_HIDDEN_WRAPPER_TOOLS = new Set([]);
|
|
363
|
-
|
|
364
|
-
export function __applyStandaloneToolDefaultsForTest(tool) {
|
|
365
|
-
if (!tool || !AGENT_HIDDEN_WRAPPER_TOOLS.has(tool.name)) return tool;
|
|
366
|
-
return {
|
|
367
|
-
...tool,
|
|
368
|
-
annotations: {
|
|
369
|
-
...(tool.annotations || {}),
|
|
370
|
-
agentHidden: true,
|
|
371
|
-
},
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
const applyStandaloneToolDefaults = __applyStandaloneToolDefaultsForTest;
|
|
375
313
|
const outputStyleStatus = (dataDir = STANDALONE_DATA_DIR) => outputStyleStatusRaw(STANDALONE_ROOT, dataDir || STANDALONE_DATA_DIR);
|
|
376
|
-
|
|
377
|
-
const ONBOARDING_VERSION = 1;
|
|
378
|
-
const QUICK_SEARCH_MODELS = Object.freeze({
|
|
379
|
-
'openai-oauth': [
|
|
380
|
-
{ id: 'gpt-5.5', display: 'GPT-5.5', latest: true, contextWindow: 1000000 },
|
|
381
|
-
{ id: 'gpt-5.4', display: 'GPT-5.4', latest: true, contextWindow: 1000000 },
|
|
382
|
-
{ id: 'gpt-5', display: 'GPT-5', contextWindow: 400000 },
|
|
383
|
-
{ id: 'gpt-4.1', display: 'GPT-4.1', contextWindow: 1000000 },
|
|
384
|
-
],
|
|
385
|
-
openai: [
|
|
386
|
-
{ id: 'gpt-5.5', display: 'GPT-5.5', latest: true, contextWindow: 1000000 },
|
|
387
|
-
{ id: 'gpt-5.4', display: 'GPT-5.4', latest: true, contextWindow: 1000000 },
|
|
388
|
-
{ id: 'gpt-5', display: 'GPT-5', contextWindow: 400000 },
|
|
389
|
-
{ id: 'gpt-4.1', display: 'GPT-4.1', contextWindow: 1000000 },
|
|
390
|
-
{ id: 'gpt-4o', display: 'GPT-4o', contextWindow: 128000 },
|
|
391
|
-
],
|
|
392
|
-
'grok-oauth': [
|
|
393
|
-
{ id: 'grok-4.3', display: 'Grok 4.3', latest: true, contextWindow: 1000000 },
|
|
394
|
-
{ id: 'grok-4.20', display: 'Grok 4.20', contextWindow: 1000000 },
|
|
395
|
-
{ id: 'grok-4', display: 'Grok 4', contextWindow: 256000 },
|
|
396
|
-
],
|
|
397
|
-
xai: [
|
|
398
|
-
{ id: 'grok-4.3', display: 'Grok 4.3', latest: true, contextWindow: 1000000 },
|
|
399
|
-
{ id: 'grok-4.20', display: 'Grok 4.20', contextWindow: 1000000 },
|
|
400
|
-
{ id: 'grok-4', display: 'Grok 4', contextWindow: 256000 },
|
|
401
|
-
],
|
|
402
|
-
gemini: [
|
|
403
|
-
{ id: 'gemini-3-pro', display: 'Gemini 3 Pro', latest: true, contextWindow: 1000000 },
|
|
404
|
-
{ id: 'gemini-2.5-pro', display: 'Gemini 2.5 Pro', contextWindow: 1000000 },
|
|
405
|
-
{ id: 'gemini-2.5-flash', display: 'Gemini 2.5 Flash', contextWindow: 1000000 },
|
|
406
|
-
{ id: 'gemini-2.0-flash', display: 'Gemini 2.0 Flash', contextWindow: 1000000 },
|
|
407
|
-
],
|
|
408
|
-
'anthropic-oauth': [
|
|
409
|
-
{ id: 'claude-opus-4-8', display: 'Claude Opus 4.8', latest: true, contextWindow: 1000000 },
|
|
410
|
-
{ id: 'claude-sonnet-4-6', display: 'Claude Sonnet 4.6', latest: true, contextWindow: 1000000 },
|
|
411
|
-
{ id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5', contextWindow: 200000 },
|
|
412
|
-
],
|
|
413
|
-
anthropic: [
|
|
414
|
-
{ id: 'claude-opus-4-8', display: 'Claude Opus 4.8', latest: true, contextWindow: 1000000 },
|
|
415
|
-
{ id: 'claude-sonnet-4-6', display: 'Claude Sonnet 4.6', latest: true, contextWindow: 1000000 },
|
|
416
|
-
{ id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5', contextWindow: 200000 },
|
|
417
|
-
],
|
|
418
|
-
});
|
|
419
314
|
// Workflow/agent pack loaders bound to this runtime's root/data layout.
|
|
420
315
|
const {
|
|
421
316
|
listWorkflowPacks,
|
|
@@ -445,15 +340,6 @@ export function __saveModelSettingsForTest(cfgMod, route, options = {}) {
|
|
|
445
340
|
return saveModelSettings(cfgMod, route, options);
|
|
446
341
|
}
|
|
447
342
|
|
|
448
|
-
function resolveCwdPath(currentCwd, value) {
|
|
449
|
-
const raw = clean(value);
|
|
450
|
-
if (!raw) throw new Error('cwd: path is required for action=set');
|
|
451
|
-
const next = resolve(currentCwd || process.cwd(), raw);
|
|
452
|
-
const stat = statSync(next);
|
|
453
|
-
if (!stat.isDirectory()) throw new Error(`cwd: not a directory: ${next}`);
|
|
454
|
-
return next;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
343
|
export async function createMixdogSessionRuntime({
|
|
458
344
|
provider,
|
|
459
345
|
model,
|
|
@@ -516,37 +402,16 @@ export async function createMixdogSessionRuntime({
|
|
|
516
402
|
let memoryModPromise = null;
|
|
517
403
|
let searchModPromise = null;
|
|
518
404
|
let codeGraphModPromise = null;
|
|
519
|
-
let outputStyleStatusCache = null;
|
|
520
|
-
let outputStyleStatusCacheAt = 0;
|
|
521
|
-
let outputStyleStatusCacheDir = '';
|
|
522
405
|
|
|
523
|
-
|
|
406
|
+
// Memory module is always-on. `memoryEnabled()` is kept as a thin alias that
|
|
407
|
+
// now always returns true (callers/compaction helpers still reference it);
|
|
408
|
+
// the user-facing toggle is `recap` (background cycles only), read via
|
|
409
|
+
// recapEnabled(config).
|
|
410
|
+
const memoryEnabled = () => true;
|
|
411
|
+
const recapEnabledFn = () => recapEnabled(config, true);
|
|
524
412
|
const channelsEnabled = () => moduleEnabled(config, 'channels', true);
|
|
525
|
-
const getOutputStyleStatusCached = ({ fresh = false } = {}) => {
|
|
526
|
-
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
527
|
-
const cacheDir = resolve(dataDir);
|
|
528
|
-
const now = performance.now();
|
|
529
|
-
if (
|
|
530
|
-
!fresh
|
|
531
|
-
&& outputStyleStatusCache
|
|
532
|
-
&& outputStyleStatusCacheDir === cacheDir
|
|
533
|
-
&& now - outputStyleStatusCacheAt < 2500
|
|
534
|
-
) {
|
|
535
|
-
return outputStyleStatusCache;
|
|
536
|
-
}
|
|
537
|
-
outputStyleStatusCache = outputStyleStatus(dataDir);
|
|
538
|
-
outputStyleStatusCacheAt = now;
|
|
539
|
-
outputStyleStatusCacheDir = cacheDir;
|
|
540
|
-
return outputStyleStatusCache;
|
|
541
|
-
};
|
|
542
|
-
const invalidateOutputStyleStatusCache = () => {
|
|
543
|
-
outputStyleStatusCache = null;
|
|
544
|
-
outputStyleStatusCacheAt = 0;
|
|
545
|
-
outputStyleStatusCacheDir = '';
|
|
546
|
-
};
|
|
547
413
|
|
|
548
414
|
async function getMemoryModule() {
|
|
549
|
-
if (!memoryEnabled()) throw new Error('memory is disabled in settings');
|
|
550
415
|
const startedAt = performance.now();
|
|
551
416
|
memoryModPromise ??= Promise.resolve(memoryRuntime);
|
|
552
417
|
const mod = await memoryModPromise;
|
|
@@ -565,211 +430,6 @@ export async function createMixdogSessionRuntime({
|
|
|
565
430
|
return mod;
|
|
566
431
|
}
|
|
567
432
|
|
|
568
|
-
function normalizeSearchAllowedDomain(site) {
|
|
569
|
-
const raw = clean(site);
|
|
570
|
-
if (!raw) return '';
|
|
571
|
-
try {
|
|
572
|
-
return new URL(/^https?:\/\//i.test(raw) ? raw : `https://${raw}`).hostname.toLowerCase();
|
|
573
|
-
} catch {
|
|
574
|
-
return raw.replace(/^https?:\/\//i, '').split('/')[0].toLowerCase();
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function nativeSearchUserLocation(locale) {
|
|
579
|
-
if (!locale || typeof locale !== 'object' || Array.isArray(locale)) return null;
|
|
580
|
-
const location = { type: 'approximate' };
|
|
581
|
-
for (const key of ['country', 'region', 'city', 'timezone']) {
|
|
582
|
-
const value = clean(locale[key]);
|
|
583
|
-
if (value) location[key] = value;
|
|
584
|
-
}
|
|
585
|
-
return Object.keys(location).length > 1 ? location : null;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function nativeSearchTool(args = {}, toolType = 'web_search', providerId = '') {
|
|
589
|
-
const providerName = normalizeSearchProviderId(providerId);
|
|
590
|
-
const domain = normalizeSearchAllowedDomain(args.site);
|
|
591
|
-
const type = clean(toolType) || 'web_search';
|
|
592
|
-
const location = nativeSearchUserLocation(args.locale);
|
|
593
|
-
if (providerName === 'gemini') {
|
|
594
|
-
return { type: type || 'google_search' };
|
|
595
|
-
}
|
|
596
|
-
if (providerName === 'anthropic' || providerName === 'anthropic-oauth') {
|
|
597
|
-
const tool = {
|
|
598
|
-
type: 'web_search_20250305',
|
|
599
|
-
name: 'web_search',
|
|
600
|
-
max_uses: Math.max(1, Math.min(10, Number(args.maxResults) || 5)),
|
|
601
|
-
};
|
|
602
|
-
if (domain) tool.allowed_domains = [domain];
|
|
603
|
-
if (location) tool.user_location = location;
|
|
604
|
-
return tool;
|
|
605
|
-
}
|
|
606
|
-
if (providerName === 'grok-oauth' || providerName === 'xai') {
|
|
607
|
-
const tool = { type };
|
|
608
|
-
if (domain) tool.filters = { allowed_domains: [domain] };
|
|
609
|
-
return tool;
|
|
610
|
-
}
|
|
611
|
-
const tool = {
|
|
612
|
-
type,
|
|
613
|
-
};
|
|
614
|
-
if (type === 'web_search') {
|
|
615
|
-
tool.search_context_size = clean(args.contextSize) || 'low';
|
|
616
|
-
if (domain) tool.filters = { allowed_domains: [domain] };
|
|
617
|
-
if (location) tool.user_location = location;
|
|
618
|
-
}
|
|
619
|
-
return tool;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function nativeSearchToolTypes(routeLike = {}) {
|
|
623
|
-
const envToolType = clean(process.env.MIXDOG_NATIVE_SEARCH_TOOL_TYPE);
|
|
624
|
-
if (envToolType) return [envToolType];
|
|
625
|
-
const configured = clean(routeLike.toolType);
|
|
626
|
-
if (configured) return [configured];
|
|
627
|
-
const providerName = normalizeSearchProviderId(routeLike.provider);
|
|
628
|
-
if (providerName === 'gemini') return ['google_search'];
|
|
629
|
-
if (providerName === 'anthropic' || providerName === 'anthropic-oauth') return ['web_search'];
|
|
630
|
-
if (providerName === 'grok-oauth' || providerName === 'xai') return ['web_search'];
|
|
631
|
-
return ['web_search', 'web_search_preview'];
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function currentMainSearchModelMeta() {
|
|
635
|
-
if (!route?.provider || !route?.model) return null;
|
|
636
|
-
return { ...route, id: route.model, display: route.model, name: route.model };
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function nativeSearchRoutes() {
|
|
640
|
-
const cfg = ensureFullConfig();
|
|
641
|
-
searchRoute = normalizeSearchRouteConfig(cfg.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
642
|
-
if (!searchRoute) return [];
|
|
643
|
-
if (isDefaultSearchRouteConfig(searchRoute)) {
|
|
644
|
-
const mainModel = currentMainSearchModelMeta();
|
|
645
|
-
if (!mainModel || !searchCapableFor(route.provider, mainModel)) return [];
|
|
646
|
-
return [{
|
|
647
|
-
key: `default\n${route.provider}\n${route.model}`,
|
|
648
|
-
provider: normalizeSearchProviderId(route.provider),
|
|
649
|
-
model: route.model,
|
|
650
|
-
source: 'default-search-route',
|
|
651
|
-
effort: route.effectiveEffort || route.effort || null,
|
|
652
|
-
fast: route.fast === true,
|
|
653
|
-
toolType: searchRoute.toolType || null,
|
|
654
|
-
}];
|
|
655
|
-
}
|
|
656
|
-
const providerName = normalizeSearchProviderId(searchRoute.provider);
|
|
657
|
-
if (!isSearchCapableProvider(providerName)) return [];
|
|
658
|
-
return [{
|
|
659
|
-
key: `${providerName}\n${searchRoute.model}`,
|
|
660
|
-
provider: providerName,
|
|
661
|
-
model: searchRoute.model,
|
|
662
|
-
source: 'search-route',
|
|
663
|
-
effort: searchRoute.effort || null,
|
|
664
|
-
fast: searchRoute.fast === true,
|
|
665
|
-
toolType: searchRoute.toolType || null,
|
|
666
|
-
}];
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function nativeSearchMessages(searchArgs = {}) {
|
|
670
|
-
const prompt = searchArgs.prompt || '';
|
|
671
|
-
return [
|
|
672
|
-
{
|
|
673
|
-
role: 'system',
|
|
674
|
-
content: [
|
|
675
|
-
'You are Mixdog native web search.',
|
|
676
|
-
'Use the hosted web_search tool for current or external facts.',
|
|
677
|
-
'Answer concisely, cite source URLs, and do not request local tools or file edits.',
|
|
678
|
-
].join('\n'),
|
|
679
|
-
},
|
|
680
|
-
{ role: 'user', content: prompt },
|
|
681
|
-
];
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function flattenNativeSearchSources(result = {}) {
|
|
685
|
-
const out = [];
|
|
686
|
-
const add = (source, fallbackTitle = '') => {
|
|
687
|
-
if (!source || typeof source !== 'object') return;
|
|
688
|
-
const url = clean(source.url || source.uri || source.href || source.source_url);
|
|
689
|
-
if (!url) return;
|
|
690
|
-
out.push({
|
|
691
|
-
title: clean(source.title || source.query || source.name || fallbackTitle || url),
|
|
692
|
-
url,
|
|
693
|
-
snippet: clean(source.snippet || source.text || source.description),
|
|
694
|
-
source: source.source || 'native-web-search',
|
|
695
|
-
provider: source.provider || 'native-web-search',
|
|
696
|
-
});
|
|
697
|
-
};
|
|
698
|
-
for (const citation of Array.isArray(result.citations) ? result.citations : []) add(citation);
|
|
699
|
-
for (const call of Array.isArray(result.webSearchCalls) ? result.webSearchCalls : []) {
|
|
700
|
-
const action = call?.action || {};
|
|
701
|
-
for (const source of Array.isArray(action.sources) ? action.sources : []) add(source, action.query || '');
|
|
702
|
-
if (action.url) add({ url: action.url, title: action.query || '' });
|
|
703
|
-
for (const url of Array.isArray(action.urls) ? action.urls : []) add({ url, title: action.query || '' });
|
|
704
|
-
}
|
|
705
|
-
const seen = new Set();
|
|
706
|
-
return out.filter((item) => {
|
|
707
|
-
const key = item.url || `${item.title}\n${item.snippet}`;
|
|
708
|
-
if (!key || seen.has(key)) return false;
|
|
709
|
-
seen.add(key);
|
|
710
|
-
return true;
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
async function runNativeWebSearch(searchArgs = {}, { signal } = {}) {
|
|
715
|
-
const candidates = nativeSearchRoutes();
|
|
716
|
-
if (!candidates.length) {
|
|
717
|
-
if (isDefaultSearchRouteConfig(searchRoute)) {
|
|
718
|
-
throw new Error(`default search route requires the current main model to support native web search (${route?.provider || 'unknown'}/${route?.model || 'unknown'})`);
|
|
719
|
-
}
|
|
720
|
-
throw new Error('search route is not configured; open /search to choose a search provider/model');
|
|
721
|
-
}
|
|
722
|
-
const errors = [];
|
|
723
|
-
for (const candidate of candidates) {
|
|
724
|
-
for (const toolType of nativeSearchToolTypes(candidate)) {
|
|
725
|
-
try {
|
|
726
|
-
await ensureProvidersReady(ensureProviderEnabled(config, candidate.provider));
|
|
727
|
-
const providerImpl = reg.getProvider(candidate.provider);
|
|
728
|
-
if (!providerImpl || typeof providerImpl.send !== 'function') {
|
|
729
|
-
throw new Error(`provider "${candidate.provider}" is not ready`);
|
|
730
|
-
}
|
|
731
|
-
const model = candidate.model;
|
|
732
|
-
const searchTool = nativeSearchTool(searchArgs, toolType, candidate.provider);
|
|
733
|
-
const startedAt = Date.now();
|
|
734
|
-
const result = await providerImpl.send(
|
|
735
|
-
nativeSearchMessages(searchArgs),
|
|
736
|
-
model,
|
|
737
|
-
undefined,
|
|
738
|
-
{
|
|
739
|
-
signal,
|
|
740
|
-
role: 'web-search',
|
|
741
|
-
sessionId: `${session?.id || 'search'}:native-search:${Date.now().toString(36)}`,
|
|
742
|
-
sourceType: 'native-search',
|
|
743
|
-
sourceName: 'search',
|
|
744
|
-
nativeTools: [searchTool],
|
|
745
|
-
nativeInclude: candidate.provider === 'openai' || candidate.provider === 'openai-oauth'
|
|
746
|
-
? ['web_search_call.action.sources']
|
|
747
|
-
: [],
|
|
748
|
-
toolChoice: candidate.provider === 'gemini' ? 'auto' : 'required',
|
|
749
|
-
...(candidate.effort ? { effort: candidate.effort } : {}),
|
|
750
|
-
fast: candidate.fast === true,
|
|
751
|
-
onStageChange: () => {},
|
|
752
|
-
onStreamDelta: () => {},
|
|
753
|
-
},
|
|
754
|
-
);
|
|
755
|
-
const sources = flattenNativeSearchSources(result);
|
|
756
|
-
return {
|
|
757
|
-
content: String(result?.content || '').trim(),
|
|
758
|
-
provider: candidate.provider,
|
|
759
|
-
model: result?.model || candidate.model || null,
|
|
760
|
-
usage: result?.usage || null,
|
|
761
|
-
citations: sources,
|
|
762
|
-
webSearchCalls: result?.webSearchCalls || [],
|
|
763
|
-
durationMs: Date.now() - startedAt,
|
|
764
|
-
};
|
|
765
|
-
} catch (err) {
|
|
766
|
-
errors.push(`${candidate.provider}${candidate.model ? `/${candidate.model}` : ''}/${toolType}: ${err?.message || String(err)}`);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
throw new Error(`native web search failed: ${errors.join(' | ')}`);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
433
|
async function getCodeGraphModule() {
|
|
774
434
|
const startedAt = performance.now();
|
|
775
435
|
codeGraphModPromise ??= import(CODE_GRAPH_RUNTIME);
|
|
@@ -808,60 +468,6 @@ export async function createMixdogSessionRuntime({
|
|
|
808
468
|
}
|
|
809
469
|
}
|
|
810
470
|
|
|
811
|
-
function formatCoreMemoryLines(payload = {}) {
|
|
812
|
-
const seen = new Set();
|
|
813
|
-
const lines = [];
|
|
814
|
-
for (const value of [
|
|
815
|
-
...(Array.isArray(payload.userLines) ? payload.userLines : []),
|
|
816
|
-
...(Array.isArray(payload.dbLines) ? payload.dbLines : []),
|
|
817
|
-
]) {
|
|
818
|
-
const text = clean(value).replace(/\s+/g, ' ');
|
|
819
|
-
if (!text) continue;
|
|
820
|
-
const key = text.toLowerCase();
|
|
821
|
-
if (seen.has(key)) continue;
|
|
822
|
-
seen.add(key);
|
|
823
|
-
lines.push(`- ${text}`);
|
|
824
|
-
if (lines.length >= 40) break;
|
|
825
|
-
}
|
|
826
|
-
const out = lines.join('\n');
|
|
827
|
-
const maxChars = 6000;
|
|
828
|
-
return out.length > maxChars ? `${out.slice(0, maxChars).replace(/\s+\S*$/, '')}\n- ...` : out;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
async function loadCoreMemoryContext() {
|
|
832
|
-
// Boot should not pay for memory/PG startup unless explicitly requested.
|
|
833
|
-
// Recall and memory tools still initialize the memory service on first use.
|
|
834
|
-
if (!memoryEnabled()) {
|
|
835
|
-
bootProfile('core-memory:disabled');
|
|
836
|
-
return '';
|
|
837
|
-
}
|
|
838
|
-
if (process.env.MIXDOG_BOOT_CORE_MEMORY !== '1') {
|
|
839
|
-
bootProfile('core-memory:skipped');
|
|
840
|
-
return '';
|
|
841
|
-
}
|
|
842
|
-
const startedAt = performance.now();
|
|
843
|
-
let timer = null;
|
|
844
|
-
const timeout = new Promise((resolve) => {
|
|
845
|
-
timer = setTimeout(() => resolve(''), 2000);
|
|
846
|
-
timer.unref?.();
|
|
847
|
-
});
|
|
848
|
-
try {
|
|
849
|
-
return await Promise.race([
|
|
850
|
-
(async () => {
|
|
851
|
-
const memoryMod = await getMemoryModule();
|
|
852
|
-
if (typeof memoryMod?.buildSessionCoreMemoryPayload !== 'function') return '';
|
|
853
|
-
return formatCoreMemoryLines(await memoryMod.buildSessionCoreMemoryPayload(currentCwd));
|
|
854
|
-
})(),
|
|
855
|
-
timeout,
|
|
856
|
-
]);
|
|
857
|
-
} catch {
|
|
858
|
-
return '';
|
|
859
|
-
} finally {
|
|
860
|
-
if (timer) clearTimeout(timer);
|
|
861
|
-
bootProfile('core-memory:done', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
|
|
865
471
|
const configStartedAt = performance.now();
|
|
866
472
|
let config = cfgMod.loadConfig({ secrets: false });
|
|
867
473
|
setConfiguredShell(normalizeSystemShellConfig(config.shell).command);
|
|
@@ -875,17 +481,26 @@ export async function createMixdogSessionRuntime({
|
|
|
875
481
|
let currentCwd = cwd;
|
|
876
482
|
let sessionNeedsCwdRefresh = false;
|
|
877
483
|
let closeRequested = false;
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
484
|
+
const warmupTimers = {
|
|
485
|
+
providerSetupWarmupTimer: null,
|
|
486
|
+
providerWarmupTimer: null,
|
|
487
|
+
providerModelWarmupTimer: null,
|
|
488
|
+
modelCatalogWarmupTimer: null,
|
|
489
|
+
statuslineUsageWarmupTimer: null,
|
|
490
|
+
statuslineUsageRefreshTimer: null,
|
|
491
|
+
};
|
|
492
|
+
// Prewarm/channel-start timer handles + async state, owned here so the
|
|
493
|
+
// teardown clearTimeout sweep still sees them; the prewarm scheduler factory
|
|
494
|
+
// mutates these objects in place (see createPrewarmSchedulers).
|
|
495
|
+
const prewarmTimers = {
|
|
496
|
+
codeGraphPrewarmTimer: null,
|
|
497
|
+
channelStartTimer: null,
|
|
498
|
+
};
|
|
499
|
+
const prewarmState = {
|
|
500
|
+
codeGraphPrewarmInFlight: false,
|
|
501
|
+
codeGraphPrewarmQueuedCwd: '',
|
|
502
|
+
channelStartPromise: null,
|
|
503
|
+
};
|
|
889
504
|
let activeTurnCount = 0;
|
|
890
505
|
let firstTurnCompleted = false;
|
|
891
506
|
function hookTranscriptPath(sessionId) {
|
|
@@ -955,20 +570,50 @@ export async function createMixdogSessionRuntime({
|
|
|
955
570
|
let codeGraphFirstTurnPrewarmDone = false;
|
|
956
571
|
const modelMetaByRoute = new Map();
|
|
957
572
|
const notificationListeners = new Set();
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
573
|
+
// Remote seat listeners (TUI): fired when remote mode flips outside a direct
|
|
574
|
+
// user action — currently only the superseded (seat stolen) path.
|
|
575
|
+
const remoteStateListeners = new Set();
|
|
576
|
+
function emitRemoteStateChange(enabled, reason = '') {
|
|
577
|
+
for (const listener of [...remoteStateListeners]) {
|
|
578
|
+
try { listener({ enabled: enabled === true, reason: String(reason || '') }); } catch {}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const providerModelCaches = {
|
|
582
|
+
providerModelsCache: { models: null, at: 0 },
|
|
583
|
+
providerModelsPromise: null,
|
|
584
|
+
providerModelsLoadSeq: 0,
|
|
585
|
+
searchProviderModelsCache: { models: null, at: 0 },
|
|
586
|
+
};
|
|
587
|
+
const providerUsageCaches = {
|
|
588
|
+
usageDashboardCache: { dashboard: null, at: 0 },
|
|
589
|
+
usageDashboardPromise: null,
|
|
590
|
+
providerSetupCache: { setup: null, at: 0 },
|
|
591
|
+
providerSetupQuickCache: { setup: null, at: 0 },
|
|
592
|
+
providerSetupPromise: null,
|
|
593
|
+
};
|
|
967
594
|
let providerInitPromise = null;
|
|
968
|
-
let mcpFailures = [];
|
|
969
595
|
let lastProjectMcpKey = null;
|
|
970
|
-
|
|
971
|
-
|
|
596
|
+
// MCP connect state, owned here so teardown/reconnect paths still observe it;
|
|
597
|
+
// the mcp-glue factory mutates this object in place (see createMcpGlue).
|
|
598
|
+
const mcpState = {
|
|
599
|
+
mcpFailures: [],
|
|
600
|
+
mcpConnectGeneration: 0,
|
|
601
|
+
mcpConnectInFlight: null,
|
|
602
|
+
};
|
|
603
|
+
// MCP glue factory — config/currentCwd live-bound; connect state shared via
|
|
604
|
+
// the caller-owned mcpState object above.
|
|
605
|
+
const {
|
|
606
|
+
mcpTransportLabel,
|
|
607
|
+
resolveEffectiveMcpServers,
|
|
608
|
+
mcpStatus,
|
|
609
|
+
connectConfiguredMcp,
|
|
610
|
+
normalizeMcpServerInput,
|
|
611
|
+
} = createMcpGlue({
|
|
612
|
+
mcpClient,
|
|
613
|
+
getConfig: () => config,
|
|
614
|
+
getCurrentCwd: () => currentCwd,
|
|
615
|
+
state: mcpState,
|
|
616
|
+
});
|
|
972
617
|
let preSessionToolSurface = null;
|
|
973
618
|
let contextStatusCacheKey = null;
|
|
974
619
|
let contextStatusCacheValue = null;
|
|
@@ -994,7 +639,7 @@ export async function createMixdogSessionRuntime({
|
|
|
994
639
|
let updateProcessState = { phase: 'idle', version: null, error: null };
|
|
995
640
|
|
|
996
641
|
function autoUpdateEnabled() {
|
|
997
|
-
return config?.update?.auto
|
|
642
|
+
return config?.update?.auto !== false;
|
|
998
643
|
}
|
|
999
644
|
|
|
1000
645
|
async function checkForUpdateInternal({ force = false } = {}) {
|
|
@@ -1037,13 +682,16 @@ export async function createMixdogSessionRuntime({
|
|
|
1037
682
|
// Non-blocking boot hook: fires after the runtime object below is fully
|
|
1038
683
|
// constructed (setTimeout(0) defers past the synchronous return), so a
|
|
1039
684
|
// slow/hanging registry request or npm install can never delay session
|
|
1040
|
-
// boot. Auto-update defaults to
|
|
1041
|
-
//
|
|
685
|
+
// boot. Auto-update defaults to ON unless config.update.auto is explicitly
|
|
686
|
+
// false; a failed check or install is
|
|
1042
687
|
// swallowed — getUpdateStatus()/getUpdateSettings() are the only surfaces,
|
|
1043
688
|
// there is no push notice channel from runtime -> TUI today.
|
|
689
|
+
// force:true — always hit the registry at boot (the 24h disk cache went
|
|
690
|
+
// stale-visible: it kept reporting an older "latest" than the installed
|
|
691
|
+
// version). checkLatestVersion() still falls back to the cache offline.
|
|
1044
692
|
const updateBootTimer = setTimeout(() => {
|
|
1045
693
|
void (async () => {
|
|
1046
|
-
await checkForUpdateInternal({ force:
|
|
694
|
+
await checkForUpdateInternal({ force: true });
|
|
1047
695
|
if (autoUpdateEnabled() && updateCheckState.updateAvailable) {
|
|
1048
696
|
await runUpdateNowInternal();
|
|
1049
697
|
}
|
|
@@ -1051,15 +699,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1051
699
|
}, 0);
|
|
1052
700
|
updateBootTimer.unref?.();
|
|
1053
701
|
|
|
1054
|
-
function mcpTransportLabel(cfg = {}) {
|
|
1055
|
-
if (cfg.autoDetect) return `autoDetect:${cfg.autoDetect}`;
|
|
1056
|
-
try {
|
|
1057
|
-
return mcpClient.resolveMcpTransportKind(cfg);
|
|
1058
|
-
} catch {
|
|
1059
|
-
return 'unknown';
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
702
|
function emitRuntimeNotification(content, meta = {}) {
|
|
1064
703
|
const text = String(content || '').trim();
|
|
1065
704
|
if (!text) return false;
|
|
@@ -1101,40 +740,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1101
740
|
};
|
|
1102
741
|
}
|
|
1103
742
|
|
|
1104
|
-
function mcpStatus() {
|
|
1105
|
-
const { servers: configured, sources } = resolveEffectiveMcpServers();
|
|
1106
|
-
const connected = new Map((mcpClient.getMcpServerStatus?.() || []).map((row) => [row.name, row]));
|
|
1107
|
-
const failures = new Map((mcpFailures || []).map((row) => [row.name, row]));
|
|
1108
|
-
const servers = [];
|
|
1109
|
-
for (const [name, cfg] of Object.entries(configured)) {
|
|
1110
|
-
const live = connected.get(name);
|
|
1111
|
-
const fail = failures.get(name);
|
|
1112
|
-
servers.push({
|
|
1113
|
-
name,
|
|
1114
|
-
configured: true,
|
|
1115
|
-
enabled: cfg?.enabled !== false,
|
|
1116
|
-
connected: Boolean(live),
|
|
1117
|
-
status: cfg?.enabled === false ? 'disabled' : live ? 'connected' : fail ? 'failed' : 'disconnected',
|
|
1118
|
-
transport: mcpTransportLabel(cfg),
|
|
1119
|
-
toolCount: live?.toolCount || 0,
|
|
1120
|
-
tools: live?.tools || [],
|
|
1121
|
-
error: fail?.msg || null,
|
|
1122
|
-
source: sources[name] || 'config',
|
|
1123
|
-
});
|
|
1124
|
-
connected.delete(name);
|
|
1125
|
-
}
|
|
1126
|
-
for (const live of connected.values()) {
|
|
1127
|
-
servers.push({ ...live, configured: false, status: 'connected' });
|
|
1128
|
-
}
|
|
1129
|
-
servers.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
1130
|
-
return {
|
|
1131
|
-
servers,
|
|
1132
|
-
configuredCount: Object.keys(configured).length,
|
|
1133
|
-
connectedCount: servers.filter((row) => row.connected).length,
|
|
1134
|
-
failedCount: servers.filter((row) => row.status === 'failed').length,
|
|
1135
|
-
};
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
743
|
function skillsStatus() {
|
|
1139
744
|
const skills = typeof contextMod.collectSkillsCached === 'function'
|
|
1140
745
|
? contextMod.collectSkillsCached(currentCwd)
|
|
@@ -1158,113 +763,51 @@ export async function createMixdogSessionRuntime({
|
|
|
1158
763
|
};
|
|
1159
764
|
}
|
|
1160
765
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
});
|
|
1207
|
-
}, delayMs);
|
|
1208
|
-
codeGraphPrewarmTimer.unref?.();
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
function applyResolvedCwd(nextCwd, { markRefresh = true } = {}) {
|
|
1212
|
-
const resolved = resolve(nextCwd);
|
|
1213
|
-
const stat = statSync(resolved);
|
|
1214
|
-
if (!stat.isDirectory()) throw new Error(`cwd: not a directory: ${resolved}`);
|
|
1215
|
-
const changed = resolve(currentCwd) !== resolved;
|
|
1216
|
-
currentCwd = resolved;
|
|
1217
|
-
process.env.MIXDOG_SESSION_CWD = currentCwd;
|
|
1218
|
-
writeLastSessionCwd(currentCwd);
|
|
1219
|
-
if (session) session.cwd = currentCwd;
|
|
1220
|
-
// cwd changes NEVER recreate the session: a mid-conversation cwd switch must
|
|
1221
|
-
// preserve the full message history (and the BP1–BP3 prompt cache). We only
|
|
1222
|
-
// retarget the live session's cwd in place; tool execution already reads the
|
|
1223
|
-
// current cwd per turn. `cwd` is intentionally absent from the prompt
|
|
1224
|
-
// context (see composeSystemPrompt), so there is nothing prompt-side to
|
|
1225
|
-
// refresh either. `markRefresh`/`changed` are kept only for signature
|
|
1226
|
-
// compatibility with existing callers.
|
|
1227
|
-
void markRefresh;
|
|
1228
|
-
// Lazy mode: before the first turn (e.g. the initial project-selection
|
|
1229
|
-
// cwd set), do NOT prewarm — that is exactly the post-first-frame freeze
|
|
1230
|
-
// we are avoiding. Once a turn has run, an in-session cwd switch DOES
|
|
1231
|
-
// prewarm the new dir, since a lookup there is now likely.
|
|
1232
|
-
if (codeGraphPrewarmLazy && !codeGraphFirstTurnPrewarmDone) {
|
|
1233
|
-
bootProfile('code-graph:prewarm-lazy', { reason: 'cwd-deferred-to-first-turn' });
|
|
1234
|
-
} else {
|
|
1235
|
-
scheduleCodeGraphPrewarm(changed ? 0 : codeGraphPrewarmDelayMs, changed ? 'cwd-change' : 'cwd');
|
|
1236
|
-
}
|
|
1237
|
-
// Project-local `.mcp.json` follows the cwd: when the effective project MCP
|
|
1238
|
-
// set changes, reconnect in the background (never await — this stays sync,
|
|
1239
|
-
// and the session is preserved). Guarded so a no-op cwd change does not
|
|
1240
|
-
// churn connections.
|
|
1241
|
-
if (changed) {
|
|
1242
|
-
try {
|
|
1243
|
-
const nextKey = resolved + '\u0000' + JSON.stringify(readProjectMcpServers(resolved));
|
|
1244
|
-
if (nextKey !== lastProjectMcpKey) {
|
|
1245
|
-
lastProjectMcpKey = nextKey;
|
|
1246
|
-
void connectConfiguredMcp({ reset: true })
|
|
1247
|
-
.then(() => invalidatePreSessionToolSurface())
|
|
1248
|
-
.catch(() => {});
|
|
1249
|
-
}
|
|
1250
|
-
} catch {}
|
|
1251
|
-
}
|
|
1252
|
-
// CwdChanged: bridge an effective cwd switch to the standard hook bus.
|
|
1253
|
-
// No matcher event — payload is minimal { cwd }. Fire-and-forget.
|
|
1254
|
-
if (changed) {
|
|
1255
|
-
try { void hooks.dispatch('CwdChanged', hookCommonPayload({ cwd: currentCwd })); } catch {}
|
|
1256
|
-
}
|
|
1257
|
-
return currentCwd;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
async function refreshSessionForCwdIfNeeded(reason = 'cwd-change') {
|
|
1261
|
-
// No-op: cwd changes are applied in place by applyResolvedCwd and never
|
|
1262
|
-
// tear down the session. Retained as a stable hook for ask()'s pre-turn
|
|
1263
|
-
// call so the surrounding turn flow is unchanged.
|
|
1264
|
-
void reason;
|
|
1265
|
-
sessionNeedsCwdRefresh = false;
|
|
1266
|
-
return session;
|
|
1267
|
-
}
|
|
766
|
+
// cwd resolution/apply + plugins-status + core-memory context. Extracted to
|
|
767
|
+
// session-runtime/cwd-plugins.mjs; the facade keeps ownership of the mutable
|
|
768
|
+
// currentCwd/session/config/lastProjectMcpKey locals via getter/setter
|
|
769
|
+
// injection and passes the later-defined callbacks (prewarm/tool-surface/
|
|
770
|
+
// memory) as closures.
|
|
771
|
+
const {
|
|
772
|
+
resolveCwdPath,
|
|
773
|
+
applyResolvedCwd,
|
|
774
|
+
refreshSessionForCwdIfNeeded,
|
|
775
|
+
pluginsStatus,
|
|
776
|
+
loadCoreMemoryContext,
|
|
777
|
+
} = createCwdPlugins({
|
|
778
|
+
getCurrentCwd: () => currentCwd,
|
|
779
|
+
setCurrentCwd: (next) => { currentCwd = next; },
|
|
780
|
+
getConfig: () => config,
|
|
781
|
+
getSession: () => session,
|
|
782
|
+
getRoute: () => route,
|
|
783
|
+
getLastProjectMcpKey: () => lastProjectMcpKey,
|
|
784
|
+
setLastProjectMcpKey: (next) => { lastProjectMcpKey = next; },
|
|
785
|
+
isCodeGraphPrewarmLazy: () => codeGraphPrewarmLazy,
|
|
786
|
+
isCodeGraphFirstTurnPrewarmDone: () => codeGraphFirstTurnPrewarmDone,
|
|
787
|
+
getCodeGraphPrewarmDelayMs: () => codeGraphPrewarmDelayMs,
|
|
788
|
+
setSessionNeedsCwdRefresh: (next) => { sessionNeedsCwdRefresh = next; },
|
|
789
|
+
connectConfiguredMcp,
|
|
790
|
+
invalidatePreSessionToolSurface: (...a) => invalidatePreSessionToolSurface(...a),
|
|
791
|
+
scheduleCodeGraphPrewarm: (...a) => scheduleCodeGraphPrewarm(...a),
|
|
792
|
+
hooks,
|
|
793
|
+
hookCommonPayload: (...a) => hookCommonPayload(...a),
|
|
794
|
+
bootProfile,
|
|
795
|
+
getMemoryModule: (...a) => getMemoryModule(...a),
|
|
796
|
+
listRegisteredPlugins,
|
|
797
|
+
pluginAdminStatus,
|
|
798
|
+
pluginManifest,
|
|
799
|
+
pluginMcpServerName,
|
|
800
|
+
mcpScriptForPlugin,
|
|
801
|
+
countSkillFiles,
|
|
802
|
+
readProjectMcpServers,
|
|
803
|
+
writeLastSessionCwd,
|
|
804
|
+
clean,
|
|
805
|
+
resolve,
|
|
806
|
+
statSync,
|
|
807
|
+
existsSync,
|
|
808
|
+
cfgMod,
|
|
809
|
+
STANDALONE_DATA_DIR,
|
|
810
|
+
});
|
|
1268
811
|
|
|
1269
812
|
function skillContent(name) {
|
|
1270
813
|
const res = typeof contextMod.loadSkillResource === 'function'
|
|
@@ -1308,166 +851,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1308
851
|
return { name, filePath };
|
|
1309
852
|
}
|
|
1310
853
|
|
|
1311
|
-
function pluginsStatus() {
|
|
1312
|
-
const dataDir = cfgMod.getPluginData?.();
|
|
1313
|
-
const configuredMcp = config?.mcpServers && typeof config.mcpServers === 'object'
|
|
1314
|
-
? config.mcpServers
|
|
1315
|
-
: {};
|
|
1316
|
-
const plugins = [];
|
|
1317
|
-
const addRegisteredPlugin = (entry) => {
|
|
1318
|
-
const root = clean(entry.root);
|
|
1319
|
-
if (!root || !existsSync(root)) return;
|
|
1320
|
-
const manifest = pluginManifest(root);
|
|
1321
|
-
const name = clean(manifest.name) || clean(manifest.id) || clean(entry.name) || root.split(/[\\/]/).pop() || root;
|
|
1322
|
-
const plugin = {
|
|
1323
|
-
id: clean(entry.id) || name,
|
|
1324
|
-
name,
|
|
1325
|
-
title: clean(manifest.title) || clean(manifest.displayName) || clean(entry.title) || name,
|
|
1326
|
-
version: clean(manifest.version) || clean(entry.version) || null,
|
|
1327
|
-
description: clean(manifest.description) || clean(entry.description),
|
|
1328
|
-
marketplace: null,
|
|
1329
|
-
source: clean(entry.sourceType) === 'local' ? 'local' : 'registry',
|
|
1330
|
-
sourceUrl: clean(entry.source),
|
|
1331
|
-
sourceType: clean(entry.sourceType) || 'git',
|
|
1332
|
-
managed: entry.managed !== false,
|
|
1333
|
-
root,
|
|
1334
|
-
installedAt: entry.installedAt || null,
|
|
1335
|
-
updatedAt: entry.updatedAt || null,
|
|
1336
|
-
skillCount: countSkillFiles(root),
|
|
1337
|
-
mcpScript: mcpScriptForPlugin(root),
|
|
1338
|
-
};
|
|
1339
|
-
plugin.mcpServerName = pluginMcpServerName(plugin);
|
|
1340
|
-
plugin.mcpEnabled = Object.prototype.hasOwnProperty.call(configuredMcp, plugin.mcpServerName)
|
|
1341
|
-
|| Object.keys(configuredMcp).some((k) => k.startsWith(`${plugin.mcpServerName}--`));
|
|
1342
|
-
plugins.push(plugin);
|
|
1343
|
-
};
|
|
1344
|
-
|
|
1345
|
-
for (const entry of listRegisteredPlugins({ dataDir })) addRegisteredPlugin(entry);
|
|
1346
|
-
|
|
1347
|
-
plugins.sort((a, b) => {
|
|
1348
|
-
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
1349
|
-
return a.name.localeCompare(b.name);
|
|
1350
|
-
});
|
|
1351
|
-
const admin = pluginAdminStatus({ dataDir });
|
|
1352
|
-
return {
|
|
1353
|
-
count: plugins.length,
|
|
1354
|
-
plugins,
|
|
1355
|
-
roots: {
|
|
1356
|
-
registry: admin.registryPath,
|
|
1357
|
-
installed: admin.installRoot,
|
|
1358
|
-
},
|
|
1359
|
-
};
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
// Merge mixdog-config `agent.mcpServers` with project-local `.mcp.json`.
|
|
1363
|
-
// On name collision the project-local `.mcp.json` entry WINS (Claude Code
|
|
1364
|
-
// precedence: project > user config). `sources[name]` records each server's
|
|
1365
|
-
// origin ('config' | 'project') for status reporting.
|
|
1366
|
-
function resolveEffectiveMcpServers() {
|
|
1367
|
-
const configured = config?.mcpServers && typeof config.mcpServers === 'object'
|
|
1368
|
-
? config.mcpServers
|
|
1369
|
-
: {};
|
|
1370
|
-
const project = readProjectMcpServers(currentCwd);
|
|
1371
|
-
const servers = { ...configured, ...project };
|
|
1372
|
-
const sources = {};
|
|
1373
|
-
for (const name of Object.keys(configured)) sources[name] = 'config';
|
|
1374
|
-
for (const name of Object.keys(project)) sources[name] = 'project';
|
|
1375
|
-
return { servers, sources };
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
async function connectConfiguredMcp({ reset = false } = {}) {
|
|
1379
|
-
// Serialize reconnects: boot connect, cwd-change reset, and rapid cwd
|
|
1380
|
-
// switches must never interleave their disconnect/connect phases, or an
|
|
1381
|
-
// older run finishing after a newer reset could re-add stale servers into
|
|
1382
|
-
// the shared client registry. Approach: a generation token + a single
|
|
1383
|
-
// in-flight promise. Each call bumps the generation, waits for any prior
|
|
1384
|
-
// run to finish, then bails if a newer call has superseded it — leaving the
|
|
1385
|
-
// latest requested effective-server-set in the registry.
|
|
1386
|
-
const gen = ++mcpConnectGeneration;
|
|
1387
|
-
if (mcpConnectInFlight) {
|
|
1388
|
-
try { await mcpConnectInFlight; } catch { /* prior run's failures already captured */ }
|
|
1389
|
-
}
|
|
1390
|
-
if (gen !== mcpConnectGeneration) return mcpStatus();
|
|
1391
|
-
const run = (async () => {
|
|
1392
|
-
if (reset) await mcpClient.disconnectAll?.();
|
|
1393
|
-
mcpFailures = [];
|
|
1394
|
-
const { servers } = resolveEffectiveMcpServers();
|
|
1395
|
-
if (Object.keys(servers).length === 0) return;
|
|
1396
|
-
try {
|
|
1397
|
-
await mcpClient.connectMcpServers(servers);
|
|
1398
|
-
} catch (error) {
|
|
1399
|
-
mcpFailures = Array.isArray(error?.failures)
|
|
1400
|
-
? error.failures
|
|
1401
|
-
: [{ name: 'mcp', msg: error?.message || String(error) }];
|
|
1402
|
-
}
|
|
1403
|
-
})();
|
|
1404
|
-
mcpConnectInFlight = run;
|
|
1405
|
-
try {
|
|
1406
|
-
await run;
|
|
1407
|
-
} finally {
|
|
1408
|
-
if (mcpConnectInFlight === run) mcpConnectInFlight = null;
|
|
1409
|
-
}
|
|
1410
|
-
return mcpStatus();
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
function normalizeMcpServerInput(input = {}) {
|
|
1414
|
-
const name = clean(input.name).toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1415
|
-
if (!name) throw new Error('MCP server name is required');
|
|
1416
|
-
const coerceStringRecord = (value) => {
|
|
1417
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
1418
|
-
const out = {};
|
|
1419
|
-
for (const [key, val] of Object.entries(value)) {
|
|
1420
|
-
if (val === undefined || val === null) continue;
|
|
1421
|
-
out[String(key)] = String(val);
|
|
1422
|
-
}
|
|
1423
|
-
return Object.keys(out).length > 0 ? out : null;
|
|
1424
|
-
};
|
|
1425
|
-
const withOptionalHeaders = (config) => {
|
|
1426
|
-
const headers = coerceStringRecord(input.headers);
|
|
1427
|
-
if (headers) config.headers = headers;
|
|
1428
|
-
return config;
|
|
1429
|
-
};
|
|
1430
|
-
const url = clean(input.url);
|
|
1431
|
-
const type = clean(input.type).toLowerCase();
|
|
1432
|
-
if (url) {
|
|
1433
|
-
if (type === 'sse') {
|
|
1434
|
-
if (!/^https?:\/\//i.test(url)) throw new Error('MCP URL must start with http:// or https://');
|
|
1435
|
-
return { name, config: withOptionalHeaders({ type: 'sse', url }) };
|
|
1436
|
-
}
|
|
1437
|
-
if (type === 'ws') {
|
|
1438
|
-
if (!/^(?:wss?|https?):\/\//i.test(url)) {
|
|
1439
|
-
throw new Error('MCP WebSocket URL must start with ws://, wss://, http://, or https://');
|
|
1440
|
-
}
|
|
1441
|
-
return { name, config: withOptionalHeaders({ type: 'ws', url }) };
|
|
1442
|
-
}
|
|
1443
|
-
if (type === 'http' || type === 'streamable-http') {
|
|
1444
|
-
if (!/^https?:\/\//i.test(url)) throw new Error('MCP URL must start with http:// or https://');
|
|
1445
|
-
return { name, config: withOptionalHeaders({ type: 'http', url }) };
|
|
1446
|
-
}
|
|
1447
|
-
if (/^wss?:\/\//i.test(url)) {
|
|
1448
|
-
return { name, config: withOptionalHeaders({ type: 'ws', url }) };
|
|
1449
|
-
}
|
|
1450
|
-
if (!/^https?:\/\//i.test(url)) throw new Error('MCP URL must start with http:// or https://');
|
|
1451
|
-
return { name, config: withOptionalHeaders({ type: 'http', url }) };
|
|
1452
|
-
}
|
|
1453
|
-
const command = clean(input.command);
|
|
1454
|
-
if (!command) throw new Error('MCP server command or URL is required');
|
|
1455
|
-
const args = Array.isArray(input.args)
|
|
1456
|
-
? input.args.map((v) => String(v)).filter(Boolean)
|
|
1457
|
-
: clean(input.args).split(/\s+/).filter(Boolean);
|
|
1458
|
-
const requestedCwd = clean(input.cwd);
|
|
1459
|
-
const cwdForServer = requestedCwd ? resolve(currentCwd, requestedCwd) : currentCwd;
|
|
1460
|
-
const root = resolve(currentCwd);
|
|
1461
|
-
const resolvedCwd = resolve(cwdForServer);
|
|
1462
|
-
if (resolvedCwd !== root && !resolvedCwd.startsWith(`${root}\\`) && !resolvedCwd.startsWith(`${root}/`)) {
|
|
1463
|
-
throw new Error('MCP server cwd must stay under the current project');
|
|
1464
|
-
}
|
|
1465
|
-
const config = { type: 'stdio', command, args, cwd: resolvedCwd };
|
|
1466
|
-
const env = coerceStringRecord(input.env);
|
|
1467
|
-
if (env) config.env = env;
|
|
1468
|
-
return { name, config };
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
854
|
const agentToolStartedAt = performance.now();
|
|
1472
855
|
const agentTool = createStandaloneAgent({
|
|
1473
856
|
cfgMod,
|
|
@@ -1508,6 +891,16 @@ export async function createMixdogSessionRuntime({
|
|
|
1508
891
|
dataDir: cfgMod.getPluginData(),
|
|
1509
892
|
cwd,
|
|
1510
893
|
onNotify: (msg) => {
|
|
894
|
+
// Single-holder remote: the worker reports it lost the bridge seat to a
|
|
895
|
+
// newer remote session. Drop remote mode entirely on this session (no
|
|
896
|
+
// handover, no retry) and tell UI listeners so the indicator updates.
|
|
897
|
+
if (msg?.method === 'notifications/mixdog/remote') {
|
|
898
|
+
if (msg?.params?.state === 'superseded' && remoteEnabled) {
|
|
899
|
+
stopRemote('superseded-by-newer-remote-session');
|
|
900
|
+
emitRemoteStateChange(false, 'superseded');
|
|
901
|
+
}
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
1511
904
|
if (msg?.method !== 'notifications/claude/channel') return;
|
|
1512
905
|
const params = msg?.params && typeof msg.params === 'object' ? msg.params : {};
|
|
1513
906
|
const meta = params.meta && typeof params.meta === 'object' ? params.meta : {};
|
|
@@ -1531,8 +924,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1531
924
|
...(channelToolDefs?.TOOL_DEFS || []).filter((tool) => channels.isChannelTool(tool?.name)),
|
|
1532
925
|
...(codeGraphToolDefs?.CODE_GRAPH_TOOL_DEFS || []).filter((tool) => tool?.name === 'code_graph'),
|
|
1533
926
|
...agentTool.tools,
|
|
1534
|
-
PROVIDER_STATUS_TOOL,
|
|
1535
|
-
CHANNEL_STATUS_TOOL,
|
|
1536
927
|
].map(applyStandaloneToolDefaults);
|
|
1537
928
|
bootProfile('tools:ready', { ms: (performance.now() - toolsStartedAt).toFixed(1), count: standaloneTools.length });
|
|
1538
929
|
|
|
@@ -1607,7 +998,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1607
998
|
if (name === 'explore') {
|
|
1608
999
|
const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
|
|
1609
1000
|
return await runExplore(args || {}, {
|
|
1610
|
-
callerCwd: args?.cwd ? resolveCwdPath(
|
|
1001
|
+
callerCwd: args?.cwd ? resolveCwdPath(args.cwd) : callerCwd,
|
|
1611
1002
|
callerSessionId,
|
|
1612
1003
|
routingSessionId: callerSessionId,
|
|
1613
1004
|
clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
|
|
@@ -1617,7 +1008,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1617
1008
|
if (name === 'cwd') {
|
|
1618
1009
|
const action = clean(args?.action || (args?.path ? 'set' : 'get')).toLowerCase();
|
|
1619
1010
|
if (action === 'set') {
|
|
1620
|
-
applyResolvedCwd(resolveCwdPath(
|
|
1011
|
+
applyResolvedCwd(resolveCwdPath(args?.path));
|
|
1621
1012
|
} else if (action !== 'get') {
|
|
1622
1013
|
throw new Error(`cwd: unknown action "${action}"`);
|
|
1623
1014
|
}
|
|
@@ -1637,8 +1028,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1637
1028
|
notifyFn: notifyFnForSession(callerSessionId),
|
|
1638
1029
|
});
|
|
1639
1030
|
}
|
|
1640
|
-
if (name === 'provider_status') return renderProviderStatus(displayConfig());
|
|
1641
|
-
if (name === 'channel_status') return renderChannelStatus();
|
|
1642
1031
|
if (channels.isChannelTool(name)) {
|
|
1643
1032
|
if (!channelsEnabled()) throw new Error('channels are disabled in settings');
|
|
1644
1033
|
return await channels.execute(name, args || {});
|
|
@@ -1660,159 +1049,58 @@ export async function createMixdogSessionRuntime({
|
|
|
1660
1049
|
}
|
|
1661
1050
|
|
|
1662
1051
|
function invalidateProviderCaches() {
|
|
1663
|
-
providerModelsCache = { models: null, at: 0 };
|
|
1664
|
-
providerModelsPromise = null;
|
|
1665
|
-
providerModelsLoadSeq += 1;
|
|
1666
|
-
searchProviderModelsCache = { models: null, at: 0 };
|
|
1667
|
-
usageDashboardCache = { dashboard: null, at: 0 };
|
|
1668
|
-
usageDashboardPromise = null;
|
|
1669
|
-
providerSetupCache = { setup: null, at: 0 };
|
|
1670
|
-
providerSetupQuickCache = { setup: null, at: 0 };
|
|
1671
|
-
providerSetupPromise = null;
|
|
1052
|
+
providerModelCaches.providerModelsCache = { models: null, at: 0 };
|
|
1053
|
+
providerModelCaches.providerModelsPromise = null;
|
|
1054
|
+
providerModelCaches.providerModelsLoadSeq += 1;
|
|
1055
|
+
providerModelCaches.searchProviderModelsCache = { models: null, at: 0 };
|
|
1056
|
+
providerUsageCaches.usageDashboardCache = { dashboard: null, at: 0 };
|
|
1057
|
+
providerUsageCaches.usageDashboardPromise = null;
|
|
1058
|
+
providerUsageCaches.providerSetupCache = { setup: null, at: 0 };
|
|
1059
|
+
providerUsageCaches.providerSetupQuickCache = { setup: null, at: 0 };
|
|
1060
|
+
providerUsageCaches.providerSetupPromise = null;
|
|
1672
1061
|
providerInitPromise = null;
|
|
1673
1062
|
modelMetaByRoute.clear();
|
|
1674
1063
|
}
|
|
1675
1064
|
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
} catch (err) {
|
|
1716
|
-
process.stderr.write(`[channels] debounced setBackend failed: ${err?.message || err}\n`);
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
// Debounced persist for the top-level `outputStyle` config key. This CANNOT
|
|
1720
|
-
// ride saveConfigAndAdopt/flushConfigSave: cfgMod.saveConfig() serializes
|
|
1721
|
-
// ONLY the agent-section fields (see orchestrator config.mjs saveConfig),
|
|
1722
|
-
// so a top-level outputStyle adopted in memory would never reach disk and
|
|
1723
|
-
// would silently revert on the next fresh catalog read/restart. Persist it
|
|
1724
|
-
// through sharedCfgMod.updateConfig (whole-root RMW under the same file
|
|
1725
|
-
// lock), debounced off the key-handler tick like the backend switch above.
|
|
1726
|
-
let pendingOutputStyleId = null;
|
|
1727
|
-
let outputStyleSaveTimer = null;
|
|
1728
|
-
function flushOutputStyleSave() {
|
|
1729
|
-
if (outputStyleSaveTimer) {
|
|
1730
|
-
clearTimeout(outputStyleSaveTimer);
|
|
1731
|
-
outputStyleSaveTimer = null;
|
|
1732
|
-
}
|
|
1733
|
-
if (pendingOutputStyleId === null) return;
|
|
1734
|
-
const styleId = pendingOutputStyleId;
|
|
1735
|
-
pendingOutputStyleId = null;
|
|
1736
|
-
try {
|
|
1737
|
-
sharedCfgMod.updateConfig((root) => {
|
|
1738
|
-
const next = { ...(root || {}), outputStyle: styleId };
|
|
1739
|
-
if (next.agent && typeof next.agent === 'object' && !Array.isArray(next.agent)) {
|
|
1740
|
-
const agent = { ...next.agent };
|
|
1741
|
-
delete agent.outputStyle;
|
|
1742
|
-
next.agent = agent;
|
|
1743
|
-
}
|
|
1744
|
-
return next;
|
|
1745
|
-
});
|
|
1746
|
-
} catch (err) {
|
|
1747
|
-
process.stderr.write(`[config] debounced outputStyle save failed: ${err?.message || err}\n`);
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
function flushConfigSave() {
|
|
1751
|
-
if (configSaveTimer) {
|
|
1752
|
-
clearTimeout(configSaveTimer);
|
|
1753
|
-
configSaveTimer = null;
|
|
1754
|
-
}
|
|
1755
|
-
if (pendingConfigToSave === null) return;
|
|
1756
|
-
const snapshot = pendingConfigToSave;
|
|
1757
|
-
pendingConfigToSave = null;
|
|
1758
|
-
try {
|
|
1759
|
-
cfgMod.saveConfig(snapshot);
|
|
1760
|
-
} catch (err) {
|
|
1761
|
-
process.stderr.write(`[config] debounced saveConfig failed: ${err?.message || err}\n`);
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
function saveConfigAndAdopt(nextConfig, { hasSecrets = configHasSecrets } = {}) {
|
|
1765
|
-
// In-memory adopt is synchronous and first so callers that read back the
|
|
1766
|
-
// value immediately (e.g. setProfile -> getProfile) see the new state.
|
|
1767
|
-
const adopted = adoptConfig(nextConfig, { hasSecrets });
|
|
1768
|
-
// Persist the adopted object; coalesce rapid successive changes into one
|
|
1769
|
-
// disk write after CONFIG_SAVE_DEBOUNCE_MS of quiet.
|
|
1770
|
-
pendingConfigToSave = config;
|
|
1771
|
-
if (configSaveTimer) clearTimeout(configSaveTimer);
|
|
1772
|
-
configSaveTimer = setTimeout(flushConfigSave, CONFIG_SAVE_DEBOUNCE_MS);
|
|
1773
|
-
configSaveTimer.unref?.();
|
|
1774
|
-
return adopted;
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
function reloadFullConfig() {
|
|
1778
|
-
// A pending debounced write holds the only copy of the latest change.
|
|
1779
|
-
// Flush it before re-reading from disk so loadConfig() observes (and the
|
|
1780
|
-
// subsequent adopt preserves) that change instead of reverting to a stale
|
|
1781
|
-
// on-disk snapshot.
|
|
1782
|
-
flushConfigSave();
|
|
1783
|
-
return adoptConfig(cfgMod.loadConfig(), { hasSecrets: true });
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
function ensureFullConfig() {
|
|
1787
|
-
if (configHasSecrets) return config;
|
|
1788
|
-
return reloadFullConfig();
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
function displayConfig() {
|
|
1792
|
-
return config;
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
function ensureConfigForRouteProvider() {
|
|
1796
|
-
const providerId = clean(route.provider);
|
|
1797
|
-
const providerCfg = config?.providers?.[providerId];
|
|
1798
|
-
if (configHasSecrets || LAZY_SECRET_PROVIDERS.has(providerId) || providerCfg?.apiKey) {
|
|
1799
|
-
return config;
|
|
1800
|
-
}
|
|
1801
|
-
return ensureFullConfig();
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
function refreshStatuslineUsageSnapshot(routeLike = {}) {
|
|
1805
|
-
const providerId = clean(routeLike.provider);
|
|
1806
|
-
const modelId = clean(routeLike.model);
|
|
1807
|
-
if (!providerId || !providerId.includes('oauth')) return;
|
|
1808
|
-
const providerObj = reg.getProvider(providerId);
|
|
1809
|
-
if (!providerObj) return;
|
|
1810
|
-
void fetchOAuthUsageSnapshot({ provider: providerId, model: modelId }, providerObj, (message) => {
|
|
1811
|
-
if (process.env.MIXDOG_STATUSLINE_TRACE) {
|
|
1812
|
-
try { process.stderr.write(`[statusline] ${message}\n`); } catch {}
|
|
1813
|
-
}
|
|
1814
|
-
}).catch(() => {});
|
|
1815
|
-
}
|
|
1065
|
+
// Config reload/save/adopt family + output-style status cache. Extracted to
|
|
1066
|
+
// session-runtime/config-lifecycle.mjs; the facade retains ownership of the
|
|
1067
|
+
// config/searchRoute/configHasSecrets mutable locals via getter/setter
|
|
1068
|
+
// injection (the proven mutable-state pattern).
|
|
1069
|
+
const {
|
|
1070
|
+
getOutputStyleStatusCached,
|
|
1071
|
+
invalidateOutputStyleStatusCache,
|
|
1072
|
+
seedOutputStyleStatusCache,
|
|
1073
|
+
adoptConfig,
|
|
1074
|
+
saveConfigAndAdopt,
|
|
1075
|
+
flushConfigSave,
|
|
1076
|
+
flushBackendSave,
|
|
1077
|
+
scheduleBackendSave,
|
|
1078
|
+
flushOutputStyleSave,
|
|
1079
|
+
scheduleOutputStyleSave,
|
|
1080
|
+
reloadFullConfig,
|
|
1081
|
+
ensureFullConfig,
|
|
1082
|
+
displayConfig,
|
|
1083
|
+
ensureConfigForRouteProvider,
|
|
1084
|
+
} = createConfigLifecycle({
|
|
1085
|
+
getConfig: () => config,
|
|
1086
|
+
setConfig: (next) => { config = next; },
|
|
1087
|
+
getSearchRoute: () => searchRoute,
|
|
1088
|
+
setSearchRoute: (next) => { searchRoute = next; },
|
|
1089
|
+
getConfigHasSecrets: () => configHasSecrets,
|
|
1090
|
+
setConfigHasSecrets: (next) => { configHasSecrets = next; },
|
|
1091
|
+
getRoute: () => route,
|
|
1092
|
+
cfgMod,
|
|
1093
|
+
sharedCfgMod,
|
|
1094
|
+
setBackend,
|
|
1095
|
+
setConfiguredShell,
|
|
1096
|
+
normalizeSystemShellConfig,
|
|
1097
|
+
normalizeSearchRouteConfig,
|
|
1098
|
+
outputStyleStatus,
|
|
1099
|
+
LAZY_SECRET_PROVIDERS,
|
|
1100
|
+
clean,
|
|
1101
|
+
resolve,
|
|
1102
|
+
STANDALONE_DATA_DIR,
|
|
1103
|
+
});
|
|
1816
1104
|
|
|
1817
1105
|
async function ensureProvidersReady(providerConfig = config.providers || {}) {
|
|
1818
1106
|
if (providerInitPromise) return await providerInitPromise;
|
|
@@ -1823,499 +1111,113 @@ export async function createMixdogSessionRuntime({
|
|
|
1823
1111
|
return await providerInitPromise;
|
|
1824
1112
|
}
|
|
1825
1113
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
return setup;
|
|
1846
|
-
})
|
|
1847
|
-
.finally(() => {
|
|
1848
|
-
providerSetupPromise = null;
|
|
1849
|
-
});
|
|
1850
|
-
return await providerSetupPromise;
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
function modelMetaKey(providerId, modelId) {
|
|
1854
|
-
return `${clean(providerId)}\n${clean(modelId)}`;
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
async function lookupModelMeta(providerId, modelId, { allowFetch = false } = {}) {
|
|
1858
|
-
const key = modelMetaKey(providerId, modelId);
|
|
1859
|
-
if (modelMetaByRoute.has(key)) return modelMetaByRoute.get(key);
|
|
1860
|
-
const providerImpl = reg.getProvider(providerId);
|
|
1861
|
-
if (!providerImpl || typeof providerImpl.listModels !== 'function') {
|
|
1862
|
-
const fallback = { id: modelId, provider: providerId };
|
|
1863
|
-
modelMetaByRoute.set(key, fallback);
|
|
1864
|
-
return fallback;
|
|
1865
|
-
}
|
|
1866
|
-
if (typeof providerImpl.getCachedModelInfo === 'function') {
|
|
1867
|
-
const cached = providerImpl.getCachedModelInfo(modelId);
|
|
1868
|
-
if (cached) {
|
|
1869
|
-
const meta = { ...cached, id: cached.id || modelId, provider: providerId };
|
|
1870
|
-
modelMetaByRoute.set(key, meta);
|
|
1871
|
-
return meta;
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
if (!allowFetch) {
|
|
1875
|
-
const fallback = { id: modelId, provider: providerId };
|
|
1876
|
-
modelMetaByRoute.set(key, fallback);
|
|
1877
|
-
scheduleProviderModelWarmup();
|
|
1878
|
-
return fallback;
|
|
1879
|
-
}
|
|
1880
|
-
try {
|
|
1881
|
-
const models = await providerImpl.listModels();
|
|
1882
|
-
const found = Array.isArray(models) ? models.find((m) => m?.id === modelId) : null;
|
|
1883
|
-
const meta = found || { id: modelId, provider: providerId };
|
|
1884
|
-
modelMetaByRoute.set(key, meta);
|
|
1885
|
-
return meta;
|
|
1886
|
-
} catch {
|
|
1887
|
-
const fallback = { id: modelId, provider: providerId };
|
|
1888
|
-
modelMetaByRoute.set(key, fallback);
|
|
1889
|
-
return fallback;
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
function parsedProviderModelVersion(id) {
|
|
1894
|
-
const text = clean(id).toLowerCase();
|
|
1895
|
-
const claude = text.match(/^claude-[a-z]+-(\d+)(?:[-.](\d+))?/);
|
|
1896
|
-
if (claude) return [Number(claude[1]) || 0, Number(claude[2]) || 0];
|
|
1897
|
-
const compact = text.match(/(?:^|[-_])(?:o|gpt|grok|qwen|llama|mistral|gemma|phi|glm)(\d+)(?:\.(\d+))?(?:\.(\d{1,3}))?/);
|
|
1898
|
-
if (compact) return compact.slice(1).filter((v) => v != null).map((v) => Number(v) || 0);
|
|
1899
|
-
const generic = text.match(/(?:^|[-_v])(\d+)(?:\.(\d+))?(?:\.(\d{1,3}))?/);
|
|
1900
|
-
return generic ? generic.slice(1).filter((v) => v != null).map((v) => Number(v) || 0) : [];
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
function compareProviderModelVersion(a, b) {
|
|
1904
|
-
const va = parsedProviderModelVersion(a.id || a.display || a.name);
|
|
1905
|
-
const vb = parsedProviderModelVersion(b.id || b.display || b.name);
|
|
1906
|
-
if (va.length === 0 && vb.length === 0) return 0;
|
|
1907
|
-
if (va.length === 0) return 1;
|
|
1908
|
-
if (vb.length === 0) return -1;
|
|
1909
|
-
for (let i = 0; i < Math.max(va.length, vb.length); i += 1) {
|
|
1910
|
-
const delta = (vb[i] || 0) - (va[i] || 0);
|
|
1911
|
-
if (delta) return delta;
|
|
1912
|
-
}
|
|
1913
|
-
return 0;
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
function providerModelReleaseTime(model) {
|
|
1917
|
-
if (model?.releaseDate) {
|
|
1918
|
-
const t = Date.parse(model.releaseDate);
|
|
1919
|
-
if (Number.isFinite(t)) return t;
|
|
1920
|
-
}
|
|
1921
|
-
const created = Number(model?.created);
|
|
1922
|
-
if (Number.isFinite(created) && created > 0) {
|
|
1923
|
-
return created < 1_000_000_000_000 ? created * 1000 : created;
|
|
1924
|
-
}
|
|
1925
|
-
const dated = clean(model?.id).match(/(?:^|-)(\d{4})(\d{2})(\d{2})(?:$|-)/);
|
|
1926
|
-
return dated ? (Date.parse(`${dated[1]}-${dated[2]}-${dated[3]}`) || 0) : 0;
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
function isClaudeProviderModel(model) {
|
|
1930
|
-
return clean(model?.provider).toLowerCase().includes('anthropic')
|
|
1931
|
-
&& /^claude-[a-z]+-/.test(clean(model?.id).toLowerCase());
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
function compareProviderModelRecency(a, b) {
|
|
1935
|
-
if (isClaudeProviderModel(a) && isClaudeProviderModel(b)) {
|
|
1936
|
-
if (a.latest !== b.latest) return a.latest ? -1 : 1;
|
|
1937
|
-
const versionDelta = compareProviderModelVersion(a, b);
|
|
1938
|
-
if (versionDelta) return versionDelta;
|
|
1939
|
-
const ta = providerModelReleaseTime(a);
|
|
1940
|
-
const tb = providerModelReleaseTime(b);
|
|
1941
|
-
if (ta !== tb) return tb - ta;
|
|
1942
|
-
return clean(a.display || a.id).localeCompare(clean(b.display || b.id));
|
|
1943
|
-
}
|
|
1944
|
-
const ta = providerModelReleaseTime(a);
|
|
1945
|
-
const tb = providerModelReleaseTime(b);
|
|
1946
|
-
if (ta !== tb) return tb - ta;
|
|
1947
|
-
if (a.latest !== b.latest) return a.latest ? -1 : 1;
|
|
1948
|
-
const versionDelta = compareProviderModelVersion(a, b);
|
|
1949
|
-
if (versionDelta) return versionDelta;
|
|
1950
|
-
return clean(a.display || a.id).localeCompare(clean(b.display || b.id));
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
function sortProviderModels(models) {
|
|
1954
|
-
return (models || []).sort((a, b) => {
|
|
1955
|
-
const ar = a.provider === route.provider ? 0 : 1;
|
|
1956
|
-
const br = b.provider === route.provider ? 0 : 1;
|
|
1957
|
-
if (ar !== br) return ar - br;
|
|
1958
|
-
if (a.provider !== b.provider) return a.provider.localeCompare(b.provider);
|
|
1959
|
-
return compareProviderModelRecency(a, b);
|
|
1960
|
-
});
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
function isSelectableLlmModel(model) {
|
|
1964
|
-
const id = clean(model?.id).toLowerCase();
|
|
1965
|
-
const display = clean(model?.display || model?.name).toLowerCase();
|
|
1966
|
-
const mode = clean(model?.mode).toLowerCase();
|
|
1967
|
-
const text = `${id} ${display}`;
|
|
1968
|
-
if (!id) return false;
|
|
1969
|
-
if (mode && !['chat', 'completion', 'responses', 'messages'].includes(mode)) return false;
|
|
1970
|
-
if (/(^|[-_\s])(image|images|video|videos|audio|tts|stt|speech|embedding|embeddings|rerank|moderation|imagine)([-_\s]|$)/i.test(text)) return false;
|
|
1971
|
-
if (/(^|[-_\s])(dall[-_\s]?e|sora|imagen)([-_\s]|$)/i.test(text)) return false;
|
|
1972
|
-
return true;
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
function providerModelCacheRow(name, m) {
|
|
1976
|
-
return {
|
|
1977
|
-
id: m.id,
|
|
1978
|
-
provider: name,
|
|
1979
|
-
display: m.display || m.name || m.id,
|
|
1980
|
-
created: typeof m.created === 'number' ? m.created : null,
|
|
1981
|
-
releaseDate: m.releaseDate || null,
|
|
1982
|
-
contextWindow: m.contextWindow,
|
|
1983
|
-
outputTokens: m.outputTokens || null,
|
|
1984
|
-
family: m.family || null,
|
|
1985
|
-
tier: m.tier || null,
|
|
1986
|
-
latest: m.latest === true,
|
|
1987
|
-
description: m.description || '',
|
|
1988
|
-
supportsVision: m.supportsVision === true,
|
|
1989
|
-
supportsFunctionCalling: m.supportsFunctionCalling === true,
|
|
1990
|
-
supportsWebSearch: searchCapableFor(name, m),
|
|
1991
|
-
supportsPromptCaching: m.supportsPromptCaching === true,
|
|
1992
|
-
supportsReasoning: m.supportsReasoning === true,
|
|
1993
|
-
reasoningLevels: Array.isArray(m.reasoningLevels) ? m.reasoningLevels : undefined,
|
|
1994
|
-
reasoningOptions: Array.isArray(m.reasoningOptions) ? m.reasoningOptions : [],
|
|
1995
|
-
reasoningContentField: m.reasoningContentField || null,
|
|
1996
|
-
mode: m.mode || null,
|
|
1997
|
-
};
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
function hydrateProviderModelRow(row) {
|
|
2001
|
-
return {
|
|
2002
|
-
...row,
|
|
2003
|
-
effortOptions: effortItemsFor(row.provider, row, null),
|
|
2004
|
-
fastCapable: fastCapableFor(row.provider, row),
|
|
2005
|
-
fastPreferred: fastPreferenceFor(config, row.provider, row.id),
|
|
2006
|
-
savedEffort: modelSettingsFor(config, row.provider, row.id).effort || null,
|
|
2007
|
-
savedFast: modelSettingsFor(config, row.provider, row.id).fast === true,
|
|
2008
|
-
};
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
function providerModelsFromCacheRows(rows) {
|
|
2012
|
-
return sortProviderModels((rows || []).map(hydrateProviderModelRow));
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
function quickProviderModelRows() {
|
|
2016
|
-
const pickerConfig = displayConfig();
|
|
2017
|
-
const rows = [];
|
|
2018
|
-
const seen = new Set();
|
|
2019
|
-
const addRoute = (routeLike = {}) => {
|
|
2020
|
-
const provider = clean(routeLike.provider);
|
|
2021
|
-
const model = clean(routeLike.model);
|
|
2022
|
-
if (!provider || !model) return;
|
|
2023
|
-
const key = `${provider}:${model}`;
|
|
2024
|
-
if (seen.has(key)) return;
|
|
2025
|
-
seen.add(key);
|
|
2026
|
-
const row = providerModelCacheRow(provider, {
|
|
2027
|
-
id: model,
|
|
2028
|
-
name: routeLike.modelDisplay || routeLike.display || model,
|
|
2029
|
-
display: routeLike.modelDisplay || routeLike.display || model,
|
|
2030
|
-
latest: routeLike.latest === true,
|
|
2031
|
-
supportsReasoning: !!routeLike.effort,
|
|
2032
|
-
mode: 'chat',
|
|
2033
|
-
});
|
|
2034
|
-
rows.push(row);
|
|
2035
|
-
modelMetaByRoute.set(modelMetaKey(provider, model), row);
|
|
2036
|
-
};
|
|
2037
|
-
|
|
2038
|
-
addRoute(route);
|
|
2039
|
-
for (const preset of pickerConfig.presets || []) addRoute(preset);
|
|
2040
|
-
for (const workflowRoute of Object.values(pickerConfig.workflowRoutes || {})) addRoute(workflowRoute);
|
|
2041
|
-
for (const agentRoute of Object.values(pickerConfig.agents || {})) addRoute(agentRoute);
|
|
2042
|
-
return providerModelsFromCacheRows(rows);
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
function addQuickSearchModel(rows, seen, provider, model) {
|
|
2046
|
-
const providerName = normalizeSearchProviderId(provider);
|
|
2047
|
-
const modelId = clean(model?.id || model);
|
|
2048
|
-
if (!providerName || !modelId || !isSearchCapableProvider(providerName)) return;
|
|
2049
|
-
const key = `${providerName}:${modelId}`;
|
|
2050
|
-
if (seen.has(key)) return;
|
|
2051
|
-
const row = providerModelCacheRow(providerName, {
|
|
2052
|
-
id: modelId,
|
|
2053
|
-
name: model?.name || model?.display || modelId,
|
|
2054
|
-
display: model?.display || model?.name || modelId,
|
|
2055
|
-
contextWindow: model?.contextWindow || null,
|
|
2056
|
-
outputTokens: model?.outputTokens || null,
|
|
2057
|
-
latest: model?.latest === true,
|
|
2058
|
-
supportsWebSearch: true,
|
|
2059
|
-
supportsFunctionCalling: model?.supportsFunctionCalling === true,
|
|
2060
|
-
supportsPromptCaching: model?.supportsPromptCaching === true,
|
|
2061
|
-
supportsReasoning: model?.supportsReasoning === true,
|
|
2062
|
-
reasoningLevels: Array.isArray(model?.reasoningLevels) ? model.reasoningLevels : undefined,
|
|
2063
|
-
reasoningOptions: Array.isArray(model?.reasoningOptions) ? model.reasoningOptions : [],
|
|
2064
|
-
mode: 'chat',
|
|
2065
|
-
});
|
|
2066
|
-
if (row.supportsWebSearch !== true) return;
|
|
2067
|
-
seen.add(key);
|
|
2068
|
-
rows.push({
|
|
2069
|
-
...row,
|
|
2070
|
-
provider: providerName,
|
|
2071
|
-
searchCapable: true,
|
|
2072
|
-
searchToolType: row.searchToolType || 'web_search',
|
|
2073
|
-
});
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
function addDefaultSearchModel(rows, seen = new Set()) {
|
|
2077
|
-
const mainModel = currentMainSearchModelMeta();
|
|
2078
|
-
if (!mainModel || !searchCapableFor(route.provider, mainModel)) return;
|
|
2079
|
-
const key = `${SEARCH_DEFAULT_PROVIDER}:${SEARCH_DEFAULT_MODEL}`;
|
|
2080
|
-
if (seen.has(key)) return;
|
|
2081
|
-
seen.add(key);
|
|
2082
|
-
rows.push({
|
|
2083
|
-
id: SEARCH_DEFAULT_MODEL,
|
|
2084
|
-
provider: SEARCH_DEFAULT_PROVIDER,
|
|
2085
|
-
display: 'Default',
|
|
2086
|
-
name: 'Default',
|
|
2087
|
-
description: `Use current main model: ${route.provider}/${route.model}`,
|
|
2088
|
-
supportsWebSearch: true,
|
|
2089
|
-
searchCapable: true,
|
|
2090
|
-
searchToolType: 'web_search',
|
|
2091
|
-
mode: 'chat',
|
|
2092
|
-
});
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
function quickSearchProviderModelRows() {
|
|
2096
|
-
const pickerConfig = displayConfig();
|
|
2097
|
-
const rows = [];
|
|
2098
|
-
const seen = new Set();
|
|
2099
|
-
addDefaultSearchModel(rows, seen);
|
|
2100
|
-
for (const [name, providerConfig] of Object.entries(pickerConfig.providers || {})) {
|
|
2101
|
-
const providerName = normalizeSearchProviderId(name);
|
|
2102
|
-
if (!providerConfig?.enabled || !isSearchCapableProvider(providerName)) continue;
|
|
2103
|
-
for (const model of QUICK_SEARCH_MODELS[providerName] || []) {
|
|
2104
|
-
addQuickSearchModel(rows, seen, providerName, model);
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
const configuredSearch = normalizeSearchRouteConfig(pickerConfig.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
2108
|
-
if (configuredSearch?.provider && configuredSearch?.model) {
|
|
2109
|
-
addQuickSearchModel(rows, seen, configuredSearch.provider, {
|
|
2110
|
-
id: configuredSearch.model,
|
|
2111
|
-
display: configuredSearch.model,
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
|
-
const mainModel = currentMainSearchModelMeta();
|
|
2115
|
-
if (mainModel && searchCapableFor(route.provider, mainModel)) {
|
|
2116
|
-
addQuickSearchModel(rows, seen, route.provider, {
|
|
2117
|
-
id: route.model,
|
|
2118
|
-
display: route.model,
|
|
2119
|
-
});
|
|
2120
|
-
}
|
|
2121
|
-
return searchModelsFromRows(rows);
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
function searchModelsFromRows(rows) {
|
|
2125
|
-
return sortProviderModels((rows || [])
|
|
2126
|
-
.filter((row) => row.supportsWebSearch === true)
|
|
2127
|
-
.map((row) => ({
|
|
2128
|
-
...row,
|
|
2129
|
-
provider: normalizeSearchProviderId(row.provider),
|
|
2130
|
-
searchCapable: true,
|
|
2131
|
-
searchToolType: row.searchToolType || 'web_search',
|
|
2132
|
-
})));
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
function searchRowsWithDefault(rows = []) {
|
|
2136
|
-
const out = [];
|
|
2137
|
-
const seen = new Set();
|
|
2138
|
-
addDefaultSearchModel(out, seen);
|
|
2139
|
-
for (const row of rows || []) {
|
|
2140
|
-
const providerName = normalizeSearchProviderId(row?.provider);
|
|
2141
|
-
const modelId = clean(row?.id || row?.model);
|
|
2142
|
-
if (providerName === SEARCH_DEFAULT_PROVIDER && modelId.toLowerCase() === SEARCH_DEFAULT_MODEL) continue;
|
|
2143
|
-
const key = `${providerName}:${modelId}`;
|
|
2144
|
-
if (!providerName || !modelId || seen.has(key)) continue;
|
|
2145
|
-
seen.add(key);
|
|
2146
|
-
out.push(row);
|
|
2147
|
-
}
|
|
2148
|
-
return out;
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
async function collectSearchProviderModels({ force = false } = {}) {
|
|
2152
|
-
if (!force && Array.isArray(searchProviderModelsCache.models)) {
|
|
2153
|
-
return providerModelsFromCacheRows(searchRowsWithDefault(searchProviderModelsCache.models));
|
|
2154
|
-
}
|
|
2155
|
-
if (!force && Array.isArray(providerModelsCache.models)) {
|
|
2156
|
-
const rows = searchRowsWithDefault(searchModelsFromRows(providerModelsCache.models));
|
|
2157
|
-
searchProviderModelsCache = { models: rows, at: Date.now() };
|
|
2158
|
-
return providerModelsFromCacheRows(rows);
|
|
2159
|
-
}
|
|
2160
|
-
if (!force) {
|
|
2161
|
-
const rows = searchRowsWithDefault(quickSearchProviderModelRows());
|
|
2162
|
-
searchProviderModelsCache = { models: rows, at: Date.now() };
|
|
2163
|
-
return providerModelsFromCacheRows(rows);
|
|
2164
|
-
}
|
|
2165
|
-
if (force) {
|
|
2166
|
-
const models = await loadSearchProviderModelsFresh({ forceRefresh: true });
|
|
2167
|
-
searchProviderModelsCache = { models, at: Date.now() };
|
|
2168
|
-
return providerModelsFromCacheRows(models);
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
function enabledSearchProviderConfig() {
|
|
2173
|
-
ensureFullConfig();
|
|
2174
|
-
const out = {};
|
|
2175
|
-
for (const [name, providerConfig] of Object.entries(config.providers || {})) {
|
|
2176
|
-
const providerName = normalizeSearchProviderId(name);
|
|
2177
|
-
if (!providerConfig?.enabled || !isSearchCapableProvider(providerName)) continue;
|
|
2178
|
-
out[providerName] = { ...providerConfig, enabled: true };
|
|
2179
|
-
}
|
|
2180
|
-
return out;
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
async function loadSearchProviderModelsFresh({ forceRefresh = false } = {}) {
|
|
2184
|
-
const searchProviders = enabledSearchProviderConfig();
|
|
2185
|
-
const providerNames = Object.keys(searchProviders);
|
|
2186
|
-
if (!providerNames.length) return [];
|
|
2187
|
-
await ensureProvidersReady(config.providers || {});
|
|
2188
|
-
const providerResults = await Promise.all(providerNames.map(async (name) => {
|
|
2189
|
-
const provider = reg.getProvider(name);
|
|
2190
|
-
if (typeof provider?.listModels !== 'function') return [];
|
|
2191
|
-
try {
|
|
2192
|
-
let models = null;
|
|
2193
|
-
if (forceRefresh && typeof provider._refreshModelCache === 'function') {
|
|
2194
|
-
models = await provider._refreshModelCache();
|
|
2195
|
-
}
|
|
2196
|
-
if (!Array.isArray(models)) {
|
|
2197
|
-
models = await provider.listModels();
|
|
2198
|
-
}
|
|
2199
|
-
if (!Array.isArray(models)) return [];
|
|
2200
|
-
const rows = [];
|
|
2201
|
-
for (const m of models) {
|
|
2202
|
-
if (!m?.id || !isSelectableLlmModel(m)) continue;
|
|
2203
|
-
const row = providerModelCacheRow(name, m);
|
|
2204
|
-
if (row.supportsWebSearch !== true) continue;
|
|
2205
|
-
rows.push({
|
|
2206
|
-
...row,
|
|
2207
|
-
provider: normalizeSearchProviderId(row.provider),
|
|
2208
|
-
searchCapable: true,
|
|
2209
|
-
searchToolType: row.searchToolType || 'web_search',
|
|
2210
|
-
});
|
|
2211
|
-
modelMetaByRoute.set(modelMetaKey(name, m.id), row);
|
|
2212
|
-
}
|
|
2213
|
-
return rows;
|
|
2214
|
-
} catch {
|
|
2215
|
-
// Keep the picker responsive if one search-capable provider has a
|
|
2216
|
-
// transient catalog/auth failure.
|
|
2217
|
-
return [];
|
|
2218
|
-
}
|
|
2219
|
-
}));
|
|
2220
|
-
const results = [];
|
|
2221
|
-
const seen = new Set();
|
|
2222
|
-
addDefaultSearchModel(results, seen);
|
|
2223
|
-
for (const row of providerResults.flat()) {
|
|
2224
|
-
const key = `${normalizeSearchProviderId(row.provider)}:${row.id}`;
|
|
2225
|
-
if (seen.has(key)) continue;
|
|
2226
|
-
seen.add(key);
|
|
2227
|
-
results.push(row);
|
|
2228
|
-
}
|
|
2229
|
-
return results;
|
|
2230
|
-
}
|
|
1114
|
+
const {
|
|
1115
|
+
currentMainSearchModelMeta,
|
|
1116
|
+
runNativeWebSearch,
|
|
1117
|
+
} = createNativeSearch({
|
|
1118
|
+
getRoute: () => route,
|
|
1119
|
+
getSearchRoute: () => searchRoute,
|
|
1120
|
+
setSearchRoute: (next) => { searchRoute = next; },
|
|
1121
|
+
getConfig: () => config,
|
|
1122
|
+
getSession: () => session,
|
|
1123
|
+
getReg: () => reg,
|
|
1124
|
+
ensureFullConfig,
|
|
1125
|
+
ensureProvidersReady,
|
|
1126
|
+
ensureProviderEnabled,
|
|
1127
|
+
normalizeSearchProviderId,
|
|
1128
|
+
normalizeSearchRouteConfig,
|
|
1129
|
+
isDefaultSearchRouteConfig,
|
|
1130
|
+
isSearchCapableProvider,
|
|
1131
|
+
searchCapableFor,
|
|
1132
|
+
});
|
|
2231
1133
|
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
return rows;
|
|
2254
|
-
} catch {
|
|
2255
|
-
// Ignore per-provider catalog failures so one bad credential or
|
|
2256
|
-
// transient /models error does not hide other authenticated models.
|
|
2257
|
-
return [];
|
|
2258
|
-
}
|
|
2259
|
-
}));
|
|
2260
|
-
const results = [];
|
|
2261
|
-
const seen = new Set();
|
|
2262
|
-
for (const row of providerResults.flat()) {
|
|
2263
|
-
const key = `${row.provider}:${row.id}`;
|
|
2264
|
-
if (seen.has(key)) continue;
|
|
2265
|
-
seen.add(key);
|
|
2266
|
-
results.push(row);
|
|
2267
|
-
modelMetaByRoute.set(modelMetaKey(row.provider, row.id), row);
|
|
2268
|
-
}
|
|
2269
|
-
return results;
|
|
2270
|
-
}
|
|
1134
|
+
// Late-bound: createWarmupSchedulers is constructed after this factory, but
|
|
1135
|
+
// cachedProviderSetup(quick) may nudge scheduleProviderSetupWarmup on a cold
|
|
1136
|
+
// quick-cache fill. Thread it by reference so the scheduler is reachable once
|
|
1137
|
+
// it exists (a pre-scheduler quick fill simply skips the warmup nudge).
|
|
1138
|
+
let scheduleProviderSetupWarmupRef = () => {};
|
|
1139
|
+
const {
|
|
1140
|
+
refreshStatuslineUsageSnapshot,
|
|
1141
|
+
cachedProviderSetup,
|
|
1142
|
+
getUsageDashboard,
|
|
1143
|
+
} = createProviderUsage({
|
|
1144
|
+
caches: providerUsageCaches,
|
|
1145
|
+
getConfig: () => config,
|
|
1146
|
+
getReg: () => reg,
|
|
1147
|
+
displayConfig,
|
|
1148
|
+
providerSetup,
|
|
1149
|
+
createUsageDashboard,
|
|
1150
|
+
fetchOAuthUsageSnapshot,
|
|
1151
|
+
isCloseRequested: () => closeRequested,
|
|
1152
|
+
getProviderSetupWarmupTimer: () => warmupTimers.providerSetupWarmupTimer,
|
|
1153
|
+
scheduleProviderSetupWarmup: (delayMs) => scheduleProviderSetupWarmupRef(delayMs),
|
|
1154
|
+
});
|
|
2271
1155
|
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
1156
|
+
// Holder filled after createQuickModelRows resolves; provider-models and
|
|
1157
|
+
// quick-model-rows are mutually dependent (rows need cache-row helpers, the
|
|
1158
|
+
// model factory needs quick fallbacks) so we thread the quick surface in by
|
|
1159
|
+
// reference after both are constructed.
|
|
1160
|
+
const providerModelQuickHelpers = {};
|
|
1161
|
+
// Late-bound: createWarmupSchedulers is constructed after this factory, but
|
|
1162
|
+
// lookupModelMeta may fire scheduleProviderModelWarmup on a cache miss. Thread
|
|
1163
|
+
// it by reference so the scheduler is called once it exists (miss handling is
|
|
1164
|
+
// best-effort; a pre-scheduler miss simply skips the warmup nudge).
|
|
1165
|
+
let scheduleProviderModelWarmupRef = () => {};
|
|
1166
|
+
const {
|
|
1167
|
+
modelMetaKey,
|
|
1168
|
+
lookupModelMeta,
|
|
1169
|
+
sortProviderModels,
|
|
1170
|
+
providerModelCacheRow,
|
|
1171
|
+
providerModelsFromCacheRows,
|
|
1172
|
+
collectSearchProviderModels,
|
|
1173
|
+
collectProviderModels,
|
|
1174
|
+
warmProviderModelCache,
|
|
1175
|
+
} = createProviderModels({
|
|
1176
|
+
caches: providerModelCaches,
|
|
1177
|
+
modelMetaByRoute,
|
|
1178
|
+
getRoute: () => route,
|
|
1179
|
+
getConfig: () => config,
|
|
1180
|
+
getReg: () => reg,
|
|
1181
|
+
searchCapableFor,
|
|
1182
|
+
sortProviderModelsRaw,
|
|
1183
|
+
providerModelCacheRowRaw,
|
|
1184
|
+
normalizeSearchProviderId,
|
|
1185
|
+
isSearchCapableProvider,
|
|
1186
|
+
ensureFullConfig,
|
|
1187
|
+
ensureProvidersReady,
|
|
1188
|
+
bootProfile,
|
|
1189
|
+
scheduleProviderModelWarmup: () => scheduleProviderModelWarmupRef(),
|
|
1190
|
+
quickHelpers: providerModelQuickHelpers,
|
|
1191
|
+
});
|
|
2299
1192
|
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
1193
|
+
const {
|
|
1194
|
+
quickProviderModelRows,
|
|
1195
|
+
addDefaultSearchModel,
|
|
1196
|
+
quickSearchProviderModelRows,
|
|
1197
|
+
searchModelsFromRows,
|
|
1198
|
+
searchRowsWithDefault,
|
|
1199
|
+
} = createQuickModelRows({
|
|
1200
|
+
getRoute: () => route,
|
|
1201
|
+
getSearchRoute: () => searchRoute,
|
|
1202
|
+
displayConfig,
|
|
1203
|
+
providerModelCacheRow,
|
|
1204
|
+
providerModelsFromCacheRows,
|
|
1205
|
+
sortProviderModels,
|
|
1206
|
+
modelMetaByRoute,
|
|
1207
|
+
modelMetaKey,
|
|
1208
|
+
normalizeSearchProviderId,
|
|
1209
|
+
normalizeSearchRouteConfig,
|
|
1210
|
+
isSearchCapableProvider,
|
|
1211
|
+
searchCapableFor,
|
|
1212
|
+
currentMainSearchModelMeta,
|
|
1213
|
+
});
|
|
1214
|
+
Object.assign(providerModelQuickHelpers, {
|
|
1215
|
+
quickProviderModelRows,
|
|
1216
|
+
addDefaultSearchModel,
|
|
1217
|
+
quickSearchProviderModelRows,
|
|
1218
|
+
searchModelsFromRows,
|
|
1219
|
+
searchRowsWithDefault,
|
|
1220
|
+
});
|
|
2319
1221
|
|
|
2320
1222
|
async function resolveMissingRouteModelForFirstTurn() {
|
|
2321
1223
|
if (routeHasModel()) return route;
|
|
@@ -2432,85 +1334,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2432
1334
|
}
|
|
2433
1335
|
session = mgr.createSession(sessionOpts);
|
|
2434
1336
|
sessionNeedsCwdRefresh = false;
|
|
2435
|
-
|
|
2436
|
-
value: (input) => hooks.beforeTool(hookCommonPayload({
|
|
2437
|
-
...input,
|
|
2438
|
-
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2439
|
-
tool_name: input?.name || input?.tool_name,
|
|
2440
|
-
tool_input: input?.args || input?.tool_input,
|
|
2441
|
-
tool_use_id: input?.toolCallId || input?.tool_use_id,
|
|
2442
|
-
cwd: input?.cwd || currentCwd,
|
|
2443
|
-
})),
|
|
2444
|
-
enumerable: false,
|
|
2445
|
-
configurable: true,
|
|
2446
|
-
writable: true,
|
|
2447
|
-
});
|
|
2448
|
-
// PostToolUse: bridge runtime tool completions to the standard hook bus.
|
|
2449
|
-
// dispatch() returns a promise; the loop's afterToolHook caller already
|
|
2450
|
-
// try/catches, so a rejection cannot escape the tool loop.
|
|
2451
|
-
Object.defineProperty(session, 'afterToolHook', {
|
|
2452
|
-
value: (input) => hooks.dispatch('PostToolUse', hookCommonPayload({
|
|
2453
|
-
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2454
|
-
cwd: input?.cwd || currentCwd,
|
|
2455
|
-
tool_name: input?.name,
|
|
2456
|
-
tool_input: input?.args,
|
|
2457
|
-
tool_use_id: input?.toolCallId || input?.tool_use_id,
|
|
2458
|
-
tool_response: input?.result,
|
|
2459
|
-
})),
|
|
2460
|
-
enumerable: false,
|
|
2461
|
-
configurable: true,
|
|
2462
|
-
writable: true,
|
|
2463
|
-
});
|
|
2464
|
-
// PostToolUseFailure: dispatched by loop.mjs only when a tool execution
|
|
2465
|
-
// resolved to a failure (thrown-error path or an is_error result). Same
|
|
2466
|
-
// shape as afterToolHook; `result` carries the error text. Best-effort.
|
|
2467
|
-
Object.defineProperty(session, 'afterToolFailureHook', {
|
|
2468
|
-
value: (input) => hooks.dispatch('PostToolUseFailure', hookCommonPayload({
|
|
2469
|
-
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2470
|
-
cwd: input?.cwd || currentCwd,
|
|
2471
|
-
tool_name: input?.name,
|
|
2472
|
-
tool_input: input?.args,
|
|
2473
|
-
tool_use_id: input?.toolCallId || input?.tool_use_id,
|
|
2474
|
-
tool_response: input?.result,
|
|
2475
|
-
})),
|
|
2476
|
-
enumerable: false,
|
|
2477
|
-
configurable: true,
|
|
2478
|
-
writable: true,
|
|
2479
|
-
});
|
|
2480
|
-
// PostToolBatch: dispatched by loop.mjs after a full parallel batch of
|
|
2481
|
-
// tool calls resolves and before the next model call. No matcher event.
|
|
2482
|
-
Object.defineProperty(session, 'afterToolBatchHook', {
|
|
2483
|
-
value: (input) => hooks.dispatch('PostToolBatch', hookCommonPayload({
|
|
2484
|
-
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2485
|
-
cwd: input?.cwd || currentCwd,
|
|
2486
|
-
})),
|
|
2487
|
-
enumerable: false,
|
|
2488
|
-
configurable: true,
|
|
2489
|
-
writable: true,
|
|
2490
|
-
});
|
|
2491
|
-
// PreCompact / PostCompact: dispatched by manager.mjs/loop.mjs compaction
|
|
2492
|
-
// flow via these session-property hooks (manager has no hooks bus access).
|
|
2493
|
-
// payload { trigger: 'auto' | 'manual' }. Best-effort.
|
|
2494
|
-
Object.defineProperty(session, 'preCompactHook', {
|
|
2495
|
-
value: (input) => hooks.dispatch('PreCompact', hookCommonPayload({
|
|
2496
|
-
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2497
|
-
cwd: input?.cwd || currentCwd,
|
|
2498
|
-
trigger: input?.trigger || 'auto',
|
|
2499
|
-
})),
|
|
2500
|
-
enumerable: false,
|
|
2501
|
-
configurable: true,
|
|
2502
|
-
writable: true,
|
|
2503
|
-
});
|
|
2504
|
-
Object.defineProperty(session, 'postCompactHook', {
|
|
2505
|
-
value: (input) => hooks.dispatch('PostCompact', hookCommonPayload({
|
|
2506
|
-
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2507
|
-
cwd: input?.cwd || currentCwd,
|
|
2508
|
-
trigger: input?.trigger || 'auto',
|
|
2509
|
-
})),
|
|
2510
|
-
enumerable: false,
|
|
2511
|
-
configurable: true,
|
|
2512
|
-
writable: true,
|
|
2513
|
-
});
|
|
1337
|
+
attachSessionHooks(session, { hooks, hookCommonPayload, getCwd: () => currentCwd });
|
|
2514
1338
|
applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
2515
1339
|
applyPreSessionToolSelection();
|
|
2516
1340
|
writeStatuslineRoute(statusRoutes, session, route);
|
|
@@ -2549,212 +1373,133 @@ function parsedProviderModelVersion(id) {
|
|
|
2549
1373
|
}
|
|
2550
1374
|
}
|
|
2551
1375
|
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
},
|
|
2626
|
-
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
function scheduleModelCatalogWarmup(delayMs = modelCatalogWarmupDelayMs) {
|
|
2630
|
-
if (!modelCatalogWarmupEnabled) {
|
|
2631
|
-
bootProfile('model-catalog:warm-skipped', { reason: 'disabled' });
|
|
2632
|
-
return;
|
|
2633
|
-
}
|
|
2634
|
-
if (modelCatalogWarmupTimer || closeRequested) return;
|
|
2635
|
-
modelCatalogWarmupTimer = setTimeout(() => {
|
|
2636
|
-
modelCatalogWarmupTimer = null;
|
|
2637
|
-
if (closeRequested) return;
|
|
2638
|
-
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
2639
|
-
bootProfile('model-catalog:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
2640
|
-
scheduleModelCatalogWarmup(backgroundBusyRetryMs);
|
|
2641
|
-
return;
|
|
2642
|
-
}
|
|
2643
|
-
void warmCatalogsInBackground()
|
|
2644
|
-
.then(() => bootProfile('model-catalog:warm-ready'))
|
|
2645
|
-
.catch((error) => bootProfile('model-catalog:warm-failed', { error: error?.message || String(error) }));
|
|
2646
|
-
}, delayMs);
|
|
2647
|
-
modelCatalogWarmupTimer.unref?.();
|
|
2648
|
-
}
|
|
1376
|
+
const {
|
|
1377
|
+
scheduleProviderWarmup,
|
|
1378
|
+
scheduleProviderSetupWarmup,
|
|
1379
|
+
scheduleProviderModelWarmup,
|
|
1380
|
+
scheduleModelCatalogWarmup,
|
|
1381
|
+
scheduleStatuslineUsageWarmup,
|
|
1382
|
+
scheduleStatuslineUsageRefresh,
|
|
1383
|
+
} = createWarmupSchedulers({
|
|
1384
|
+
timers: warmupTimers,
|
|
1385
|
+
bootProfile,
|
|
1386
|
+
getRoute: () => route,
|
|
1387
|
+
getConfig: () => config,
|
|
1388
|
+
isCloseRequested: () => closeRequested,
|
|
1389
|
+
getActiveTurnCount: () => activeTurnCount,
|
|
1390
|
+
getSessionCreatePromise: () => sessionCreatePromise,
|
|
1391
|
+
getProviderModelsCache: () => providerModelCaches.providerModelsCache,
|
|
1392
|
+
getProviderModelsPromise: () => providerModelCaches.providerModelsPromise,
|
|
1393
|
+
reloadFullConfig,
|
|
1394
|
+
ensureConfigForRouteProvider,
|
|
1395
|
+
ensureProvidersReady,
|
|
1396
|
+
ensureProviderEnabled,
|
|
1397
|
+
refreshStatuslineUsageSnapshot,
|
|
1398
|
+
warmProviderModelCache,
|
|
1399
|
+
cachedProviderSetup,
|
|
1400
|
+
warmCatalogsInBackground,
|
|
1401
|
+
isFirstTurnCompleted: () => firstTurnCompleted,
|
|
1402
|
+
envFlag,
|
|
1403
|
+
delays: {
|
|
1404
|
+
providerWarmupDelayMs,
|
|
1405
|
+
providerSetupWarmupDelayMs,
|
|
1406
|
+
providerModelWarmupDelayMs,
|
|
1407
|
+
modelCatalogWarmupDelayMs,
|
|
1408
|
+
statuslineUsageWarmupDelayMs,
|
|
1409
|
+
statuslineUsageRefreshDelayMs,
|
|
1410
|
+
backgroundBusyRetryMs,
|
|
1411
|
+
},
|
|
1412
|
+
flags: {
|
|
1413
|
+
providerWarmupEnabled,
|
|
1414
|
+
modelPrefetchEnabled,
|
|
1415
|
+
modelCatalogWarmupEnabled,
|
|
1416
|
+
},
|
|
1417
|
+
});
|
|
1418
|
+
scheduleProviderModelWarmupRef = scheduleProviderModelWarmup;
|
|
1419
|
+
scheduleProviderSetupWarmupRef = scheduleProviderSetupWarmup;
|
|
1420
|
+
|
|
1421
|
+
const {
|
|
1422
|
+
scheduleCodeGraphPrewarm,
|
|
1423
|
+
scheduleLeadSessionPrewarm,
|
|
1424
|
+
invokeChannelStart,
|
|
1425
|
+
scheduleChannelStart,
|
|
1426
|
+
} = createPrewarmSchedulers({
|
|
1427
|
+
timers: prewarmTimers,
|
|
1428
|
+
bootProfile,
|
|
1429
|
+
getCurrentCwd: () => currentCwd,
|
|
1430
|
+
isCloseRequested: () => closeRequested,
|
|
1431
|
+
getActiveTurnCount: () => activeTurnCount,
|
|
1432
|
+
getSessionCreatePromise: () => sessionCreatePromise,
|
|
1433
|
+
getSession: () => session,
|
|
1434
|
+
isRemoteEnabled: () => remoteEnabled,
|
|
1435
|
+
channelsEnabled,
|
|
1436
|
+
getCodeGraphModule,
|
|
1437
|
+
createCurrentSession,
|
|
1438
|
+
channels,
|
|
1439
|
+
envFlag,
|
|
1440
|
+
delays: {
|
|
1441
|
+
codeGraphPrewarmDelayMs,
|
|
1442
|
+
channelStartDelayMs,
|
|
1443
|
+
sessionPrewarmDelayMs,
|
|
1444
|
+
backgroundBusyRetryMs,
|
|
1445
|
+
},
|
|
1446
|
+
flags: {
|
|
1447
|
+
codeGraphPrewarmEnabled,
|
|
1448
|
+
sessionPrewarmEnabled,
|
|
1449
|
+
},
|
|
1450
|
+
state: prewarmState,
|
|
1451
|
+
});
|
|
2649
1452
|
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
2661
|
-
bootProfile('statusline-usage:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
2662
|
-
scheduleStatuslineUsageWarmup(backgroundBusyRetryMs);
|
|
2663
|
-
return;
|
|
2664
|
-
}
|
|
1453
|
+
// Eagerly create/refresh the remote transcript writer for the CURRENT
|
|
1454
|
+
// session + cwd, publish the session record, and ensure the transcript
|
|
1455
|
+
// file exists on disk. Called from startRemote() (so the channel worker's
|
|
1456
|
+
// activate-time discovery finds THIS session immediately instead of
|
|
1457
|
+
// waiting for the first turn) and from ask() at turn start. Returns true
|
|
1458
|
+
// when a writer is bound.
|
|
1459
|
+
function ensureRemoteTranscriptWriter() {
|
|
1460
|
+
if (!remoteEnabled || !session?.id) return false;
|
|
1461
|
+
const twKey = `${session.id}\u0000${currentCwd}`;
|
|
1462
|
+
if (_twKey !== twKey) {
|
|
2665
1463
|
try {
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
1464
|
+
_transcriptWriter = createTranscriptWriter({
|
|
1465
|
+
mixdogHome: mixdogHome(),
|
|
1466
|
+
sessionId: session.id,
|
|
1467
|
+
cwd: currentCwd,
|
|
1468
|
+
pid: process.pid,
|
|
1469
|
+
});
|
|
1470
|
+
_transcriptWriter.writeSessionRecord();
|
|
1471
|
+
_twKey = twKey;
|
|
2671
1472
|
} catch (error) {
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
}, delayMs);
|
|
2677
|
-
statuslineUsageWarmupTimer.unref?.();
|
|
2678
|
-
}
|
|
2679
|
-
|
|
2680
|
-
// Idle keep-alive loop: periodically re-fetch the OAuth usage snapshot so its
|
|
2681
|
-
// cachedAt stays "live-fresh" and the statusline usage segment does not vanish
|
|
2682
|
-
// after LIVE_USAGE_SNAPSHOT_MAX_AGE_MS while the session is idle. Turn-driven
|
|
2683
|
-
// refreshes (recordStandaloneStatusTelemetry) already cover active sessions.
|
|
2684
|
-
function scheduleStatuslineUsageRefresh(delayMs = statuslineUsageRefreshDelayMs) {
|
|
2685
|
-
const providerId = clean(route?.provider);
|
|
2686
|
-
if (!providerId || !providerId.includes('oauth')) return;
|
|
2687
|
-
if (statuslineUsageRefreshTimer || closeRequested) return;
|
|
2688
|
-
statuslineUsageRefreshTimer = setTimeout(async () => {
|
|
2689
|
-
statuslineUsageRefreshTimer = null;
|
|
2690
|
-
if (closeRequested) return;
|
|
2691
|
-
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
2692
|
-
// Active turns refresh usage on their own; just re-arm the idle loop.
|
|
2693
|
-
scheduleStatuslineUsageRefresh();
|
|
2694
|
-
return;
|
|
1473
|
+
process.stderr.write(`mixdog: transcript-writer: init failed: ${error?.message || error}\n`);
|
|
1474
|
+
_transcriptWriter = null;
|
|
1475
|
+
_twKey = '';
|
|
1476
|
+
return false;
|
|
2695
1477
|
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
refreshStatuslineUsageSnapshot(route);
|
|
2701
|
-
} catch {
|
|
2702
|
-
// Usage display must never affect the session runtime.
|
|
2703
|
-
} finally {
|
|
2704
|
-
scheduleStatuslineUsageRefresh();
|
|
2705
|
-
}
|
|
2706
|
-
}, delayMs);
|
|
2707
|
-
statuslineUsageRefreshTimer.unref?.();
|
|
2708
|
-
}
|
|
2709
|
-
|
|
2710
|
-
function invokeChannelStart() {
|
|
2711
|
-
if (channelStartPromise) return channelStartPromise;
|
|
2712
|
-
const startedAt = performance.now();
|
|
2713
|
-
bootProfile('channels:start:begin');
|
|
2714
|
-
channelStartPromise = channels.start()
|
|
2715
|
-
.then(() => bootProfile('channels:start:ready', { ms: (performance.now() - startedAt).toFixed(1) }))
|
|
2716
|
-
.catch((error) => bootProfile('channels:start:failed', {
|
|
2717
|
-
ms: (performance.now() - startedAt).toFixed(1),
|
|
2718
|
-
error: error?.message || String(error),
|
|
2719
|
-
}))
|
|
2720
|
-
.finally(() => {
|
|
2721
|
-
channelStartPromise = null;
|
|
2722
|
-
});
|
|
2723
|
-
return channelStartPromise;
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
function scheduleChannelStart(delayMs = channelStartDelayMs) {
|
|
2727
|
-
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
2728
|
-
bootProfile('channels:start-skipped');
|
|
2729
|
-
return;
|
|
2730
|
-
}
|
|
2731
|
-
if (!channelsEnabled()) {
|
|
2732
|
-
bootProfile('channels:start-disabled');
|
|
2733
|
-
return;
|
|
1478
|
+
} else {
|
|
1479
|
+
// Same binding — refresh updatedAt so worker-side discovery keeps
|
|
1480
|
+
// ranking this session as the live parent-chain candidate.
|
|
1481
|
+
try { _transcriptWriter?.writeSessionRecord(); } catch {}
|
|
2734
1482
|
}
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
channelStartTimer = null;
|
|
2739
|
-
if (closeRequested) return;
|
|
2740
|
-
// A deferred start may straddle a stopRemote(); re-check before booting so
|
|
2741
|
-
// a turned-off session neither starts channels nor keeps rescheduling.
|
|
2742
|
-
if (!remoteEnabled) return;
|
|
2743
|
-
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
2744
|
-
bootProfile('channels:start-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
2745
|
-
scheduleChannelStart(backgroundBusyRetryMs);
|
|
2746
|
-
return;
|
|
2747
|
-
}
|
|
2748
|
-
void invokeChannelStart();
|
|
2749
|
-
}, delayMs);
|
|
2750
|
-
channelStartTimer.unref?.();
|
|
1483
|
+
try { _transcriptWriter?.ensureTranscriptFile(); }
|
|
1484
|
+
catch (error) { process.stderr.write(`mixdog: transcript-writer: ensureTranscriptFile failed: ${error?.message || error}\n`); }
|
|
1485
|
+
return _transcriptWriter != null;
|
|
2751
1486
|
}
|
|
2752
1487
|
|
|
2753
1488
|
// Remote (Discord channel) mode is opt-in per session. Only a session that
|
|
2754
1489
|
// explicitly enables remote — via `mixdog --remote` or the runtime toggle —
|
|
2755
1490
|
// boots the channel worker and contends for channel ownership.
|
|
1491
|
+
// startRemote() is FORCE-TAKEOVER: it always (re)claims the bridge seat and
|
|
1492
|
+
// rebinds output forwarding to this session, even when the worker is
|
|
1493
|
+
// already running (e.g. `/remote` re-issued after another session took the
|
|
1494
|
+
// seat, or to re-pin forwarding onto the current transcript).
|
|
2756
1495
|
function startRemote() {
|
|
2757
1496
|
remoteEnabled = true;
|
|
1497
|
+
// Publish this session's record + transcript file BEFORE the worker's
|
|
1498
|
+
// activate-time discovery polls, so output forwarding binds to this
|
|
1499
|
+
// terminal session immediately instead of waiting for the first turn.
|
|
1500
|
+
// No-op when the session has not been created yet (lazy mode); that
|
|
1501
|
+
// case is covered by the turn-start rebind in ask().
|
|
1502
|
+
ensureRemoteTranscriptWriter();
|
|
2758
1503
|
// A backend switch may still be sitting in its debounce window; flush it
|
|
2759
1504
|
// so the channel worker boots against the backend the user just chose,
|
|
2760
1505
|
// not the previous on-disk value.
|
|
@@ -2768,19 +1513,43 @@ function parsedProviderModelVersion(id) {
|
|
|
2768
1513
|
return true;
|
|
2769
1514
|
}
|
|
2770
1515
|
if (closeRequested) return true;
|
|
2771
|
-
if (channelStartTimer) {
|
|
2772
|
-
clearTimeout(channelStartTimer);
|
|
2773
|
-
channelStartTimer = null;
|
|
1516
|
+
if (prewarmTimers.channelStartTimer) {
|
|
1517
|
+
clearTimeout(prewarmTimers.channelStartTimer);
|
|
1518
|
+
prewarmTimers.channelStartTimer = null;
|
|
2774
1519
|
}
|
|
2775
1520
|
bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
|
|
2776
|
-
void
|
|
1521
|
+
void (async () => {
|
|
1522
|
+
// Immediate-occupancy guarantee: make sure a session + transcript
|
|
1523
|
+
// exist BEFORE the worker boots — a freshly-forked worker claims and
|
|
1524
|
+
// runs transcript discovery inside its own start(), so publishing the
|
|
1525
|
+
// session record/file first lets that very first discovery pass bind
|
|
1526
|
+
// output forwarding to THIS terminal instead of a persisted/stale
|
|
1527
|
+
// neighbour. Lazy mode means the session may not exist yet at /remote
|
|
1528
|
+
// time; create it here (idempotent — reuses a live session, joins an
|
|
1529
|
+
// in-flight create). On create failure we still claim: that matches
|
|
1530
|
+
// the pre-eager behavior (bind resolves on the first turn's rebind).
|
|
1531
|
+
try { await createCurrentSession('remote-start'); }
|
|
1532
|
+
catch (error) { bootProfile('channels:remote-session-create-failed', { error: error?.message || String(error) }); }
|
|
1533
|
+
ensureRemoteTranscriptWriter();
|
|
1534
|
+
// Re-check after the awaits above: stopRemote()/superseded or runtime
|
|
1535
|
+
// close may have landed mid-chain — do not boot/claim for a session
|
|
1536
|
+
// that already turned remote off.
|
|
1537
|
+
if (!remoteEnabled || closeRequested) return;
|
|
1538
|
+
await invokeChannelStart();
|
|
1539
|
+
if (!remoteEnabled || closeRequested) return;
|
|
1540
|
+
// Unconditional claim + forwarder rebind. A freshly-forked worker
|
|
1541
|
+
// already claims in its own start(), so this is idempotent there; the
|
|
1542
|
+
// already-running case is where it matters (last-wins seat overwrite +
|
|
1543
|
+
// transcript rebind onto this session).
|
|
1544
|
+
await channels.execute('activate_channel_bridge', { active: true });
|
|
1545
|
+
})().catch((error) => bootProfile('channels:claim-failed', { error: error?.message || String(error) }));
|
|
2777
1546
|
return true;
|
|
2778
1547
|
}
|
|
2779
1548
|
|
|
2780
1549
|
function stopRemote(reason) {
|
|
2781
1550
|
remoteEnabled = false;
|
|
2782
1551
|
// Cancel any pending deferred start so it can't fire after remote is off.
|
|
2783
|
-
if (channelStartTimer) { clearTimeout(channelStartTimer); channelStartTimer = null; }
|
|
1552
|
+
if (prewarmTimers.channelStartTimer) { clearTimeout(prewarmTimers.channelStartTimer); prewarmTimers.channelStartTimer = null; }
|
|
2784
1553
|
channels.stop(reason || 'remote-disabled', { waitForExit: false }).catch(() => {});
|
|
2785
1554
|
return true;
|
|
2786
1555
|
}
|
|
@@ -2879,7 +1648,64 @@ function parsedProviderModelVersion(id) {
|
|
|
2879
1648
|
return true;
|
|
2880
1649
|
}
|
|
2881
1650
|
|
|
1651
|
+
// Pure settings-delegate methods (onboarding status/skip, autoClear, profile,
|
|
1652
|
+
// compaction, recap/memory, channels, systemShell, update settings, channel
|
|
1653
|
+
// token save/forget, setBackend). Extracted to session-runtime/settings-api.mjs
|
|
1654
|
+
// and SPREAD into the API object below so the external surface is unchanged.
|
|
1655
|
+
const settingsApi = createSettingsApi({
|
|
1656
|
+
getConfig: () => config,
|
|
1657
|
+
getRoute: () => route,
|
|
1658
|
+
getSession: () => session,
|
|
1659
|
+
getRemoteEnabled: () => remoteEnabled,
|
|
1660
|
+
saveConfigAndAdopt,
|
|
1661
|
+
scheduleBackendSave,
|
|
1662
|
+
cfgMod,
|
|
1663
|
+
hasOwn,
|
|
1664
|
+
normalizeAutoClearConfig,
|
|
1665
|
+
autoClearIdleMsForProvider,
|
|
1666
|
+
autoClearProviderDefaults,
|
|
1667
|
+
normalizeCompactionConfig,
|
|
1668
|
+
normalizeCompactTypeSetting,
|
|
1669
|
+
normalizeSystemShellConfig,
|
|
1670
|
+
normalizeSystemShellCommand,
|
|
1671
|
+
setConfiguredShell,
|
|
1672
|
+
setRecapEnabledInConfig,
|
|
1673
|
+
setModuleEnabledInConfig,
|
|
1674
|
+
summarizeWorkflowRoutes,
|
|
1675
|
+
parseDurationMs,
|
|
1676
|
+
formatDurationMs,
|
|
1677
|
+
localPackageVersion,
|
|
1678
|
+
memoryEnabled,
|
|
1679
|
+
recapEnabledFn,
|
|
1680
|
+
channelsEnabled,
|
|
1681
|
+
autoUpdateEnabled,
|
|
1682
|
+
getUpdateCheckState: () => updateCheckState,
|
|
1683
|
+
getUpdateProcessState: () => updateProcessState,
|
|
1684
|
+
invalidateContextStatusCache: (...a) => invalidateContextStatusCache(...a),
|
|
1685
|
+
invalidatePreSessionToolSurface: (...a) => invalidatePreSessionToolSurface(...a),
|
|
1686
|
+
scheduleChannelStart: (...a) => scheduleChannelStart(...a),
|
|
1687
|
+
channels,
|
|
1688
|
+
clearChannelStartTimer: () => {
|
|
1689
|
+
if (prewarmTimers.channelStartTimer) {
|
|
1690
|
+
clearTimeout(prewarmTimers.channelStartTimer);
|
|
1691
|
+
prewarmTimers.channelStartTimer = null;
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
checkForUpdateInternal: (...a) => checkForUpdateInternal(...a),
|
|
1695
|
+
runUpdateNowInternal: (...a) => runUpdateNowInternal(...a),
|
|
1696
|
+
reloadChannelsSoon: (...a) => reloadChannelsSoon(...a),
|
|
1697
|
+
ONBOARDING_VERSION,
|
|
1698
|
+
saveDiscordToken,
|
|
1699
|
+
forgetDiscordToken,
|
|
1700
|
+
saveTelegramToken,
|
|
1701
|
+
forgetTelegramToken,
|
|
1702
|
+
saveWebhookAuthtoken,
|
|
1703
|
+
forgetWebhookAuthtoken,
|
|
1704
|
+
setBackend,
|
|
1705
|
+
});
|
|
1706
|
+
|
|
2882
1707
|
return {
|
|
1708
|
+
...settingsApi,
|
|
2883
1709
|
get id() {
|
|
2884
1710
|
return session?.id || null;
|
|
2885
1711
|
},
|
|
@@ -2914,7 +1740,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2914
1740
|
return mode;
|
|
2915
1741
|
},
|
|
2916
1742
|
get autoClear() {
|
|
2917
|
-
return
|
|
1743
|
+
return this.getAutoClear();
|
|
2918
1744
|
},
|
|
2919
1745
|
get systemShell() {
|
|
2920
1746
|
return normalizeSystemShellConfig(config.shell);
|
|
@@ -2925,10 +1751,14 @@ function parsedProviderModelVersion(id) {
|
|
|
2925
1751
|
},
|
|
2926
1752
|
get workflow() {
|
|
2927
1753
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
1754
|
+
const active = activeWorkflowSummary(config, dataDir);
|
|
2928
1755
|
if (session?.workflow && typeof session.workflow === 'object') {
|
|
2929
|
-
|
|
1756
|
+
const current = workflowSummary(session.workflow);
|
|
1757
|
+
return current?.id && active?.id && current.id !== active.id
|
|
1758
|
+
? { ...active, currentSession: current, appliedToCurrentSession: false }
|
|
1759
|
+
: active;
|
|
2930
1760
|
}
|
|
2931
|
-
return
|
|
1761
|
+
return active;
|
|
2932
1762
|
},
|
|
2933
1763
|
get outputStyle() {
|
|
2934
1764
|
return getOutputStyleStatusCached().current;
|
|
@@ -3046,244 +1876,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3046
1876
|
return await cachedProviderSetup();
|
|
3047
1877
|
},
|
|
3048
1878
|
async getUsageDashboard(options = {}) {
|
|
3049
|
-
|
|
3050
|
-
if (!forceSetup && usageDashboardCache.dashboard) {
|
|
3051
|
-
const cached = {
|
|
3052
|
-
...usageDashboardCache.dashboard,
|
|
3053
|
-
refresh: false,
|
|
3054
|
-
checking: false,
|
|
3055
|
-
cached: true,
|
|
3056
|
-
cachedAt: usageDashboardCache.at,
|
|
3057
|
-
};
|
|
3058
|
-
if (typeof options?.onUpdate === 'function') {
|
|
3059
|
-
try { options.onUpdate(cached); } catch {}
|
|
3060
|
-
}
|
|
3061
|
-
return cached;
|
|
3062
|
-
}
|
|
3063
|
-
if (!forceSetup && usageDashboardPromise) return await usageDashboardPromise;
|
|
3064
|
-
const quickSetup = options?.quickSetup !== false;
|
|
3065
|
-
const getProvider = (providerId) => reg.getProvider(providerId);
|
|
3066
|
-
const log = (message) => {
|
|
3067
|
-
if (process.env.MIXDOG_USAGE_TRACE) {
|
|
3068
|
-
try { process.stderr.write(`[usage] ${message}\n`); } catch {}
|
|
3069
|
-
}
|
|
3070
|
-
};
|
|
3071
|
-
if (quickSetup && typeof options?.onUpdate === 'function') {
|
|
3072
|
-
const previewConfig = displayConfig();
|
|
3073
|
-
const previewSetup = await cachedProviderSetup({ force: false, quick: true });
|
|
3074
|
-
await createUsageDashboard(previewConfig, {
|
|
3075
|
-
...(options || {}),
|
|
3076
|
-
preview: true,
|
|
3077
|
-
setup: previewSetup,
|
|
3078
|
-
getProvider,
|
|
3079
|
-
log,
|
|
3080
|
-
});
|
|
3081
|
-
}
|
|
3082
|
-
const buildDashboard = async () => {
|
|
3083
|
-
const dashboard = await createUsageDashboard(displayConfig(), {
|
|
3084
|
-
...(options || {}),
|
|
3085
|
-
setup: await cachedProviderSetup({ force: forceSetup, quick: false }),
|
|
3086
|
-
getProvider,
|
|
3087
|
-
log,
|
|
3088
|
-
});
|
|
3089
|
-
usageDashboardCache = { dashboard, at: Date.now() };
|
|
3090
|
-
return dashboard;
|
|
3091
|
-
};
|
|
3092
|
-
if (forceSetup) return await buildDashboard();
|
|
3093
|
-
usageDashboardPromise = buildDashboard()
|
|
3094
|
-
.finally(() => {
|
|
3095
|
-
usageDashboardPromise = null;
|
|
3096
|
-
});
|
|
3097
|
-
return await usageDashboardPromise;
|
|
3098
|
-
},
|
|
3099
|
-
getOnboardingStatus() {
|
|
3100
|
-
const nextConfig = displayConfig();
|
|
3101
|
-
return {
|
|
3102
|
-
completed: nextConfig?.onboarding?.completed === true,
|
|
3103
|
-
version: nextConfig?.onboarding?.version || 0,
|
|
3104
|
-
default: nextConfig?.default || null,
|
|
3105
|
-
workflowRoutes: summarizeWorkflowRoutes(nextConfig),
|
|
3106
|
-
};
|
|
3107
|
-
},
|
|
3108
|
-
// Mark onboarding as done WITHOUT touching routes/agents/provider. Used by
|
|
3109
|
-
// the TUI "skip" (Esc) path so the wizard doesn't reappear next launch,
|
|
3110
|
-
// while leaving any existing config routes untouched.
|
|
3111
|
-
skipOnboarding() {
|
|
3112
|
-
const nextConfig = { ...config };
|
|
3113
|
-
nextConfig.onboarding = {
|
|
3114
|
-
...(nextConfig.onboarding || {}),
|
|
3115
|
-
completed: true,
|
|
3116
|
-
version: ONBOARDING_VERSION,
|
|
3117
|
-
completedAt: new Date().toISOString(),
|
|
3118
|
-
skipped: true,
|
|
3119
|
-
};
|
|
3120
|
-
saveConfigAndAdopt(nextConfig);
|
|
3121
|
-
return this.getOnboardingStatus();
|
|
3122
|
-
},
|
|
3123
|
-
getAutoClear() {
|
|
3124
|
-
return normalizeAutoClearConfig(config.autoClear);
|
|
3125
|
-
},
|
|
3126
|
-
// --- User profile (/profile statusline command) ---------------------
|
|
3127
|
-
// getProfile returns the normalized { title, language } plus the resolved
|
|
3128
|
-
// language catalog entry and the full language list for the picker UI.
|
|
3129
|
-
getProfile() {
|
|
3130
|
-
const profile = cfgMod.normalizeProfileConfig(config.profile);
|
|
3131
|
-
return {
|
|
3132
|
-
...profile,
|
|
3133
|
-
languageEntry: cfgMod.profileLanguageEntry(profile.language),
|
|
3134
|
-
languages: cfgMod.PROFILE_LANGUAGES,
|
|
3135
|
-
};
|
|
3136
|
-
},
|
|
3137
|
-
// setProfile patches title and/or language and persists. Unknown language
|
|
3138
|
-
// ids normalize back to 'system'. Prompt-side injection is wired separately
|
|
3139
|
-
// (composeSystemPrompt) — this only owns the stored value.
|
|
3140
|
-
setProfile(input = {}) {
|
|
3141
|
-
const current = cfgMod.normalizeProfileConfig(config.profile);
|
|
3142
|
-
const next = { ...current };
|
|
3143
|
-
if (hasOwn(input, 'title') || hasOwn(input, 'name')) {
|
|
3144
|
-
next.title = input.title ?? input.name ?? '';
|
|
3145
|
-
}
|
|
3146
|
-
if (hasOwn(input, 'language') || hasOwn(input, 'lang')) {
|
|
3147
|
-
next.language = input.language ?? input.lang ?? 'system';
|
|
3148
|
-
}
|
|
3149
|
-
const normalized = cfgMod.normalizeProfileConfig(next);
|
|
3150
|
-
saveConfigAndAdopt({ ...config, profile: normalized });
|
|
3151
|
-
return this.getProfile();
|
|
3152
|
-
},
|
|
3153
|
-
getCompactionSettings() {
|
|
3154
|
-
return normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
|
|
3155
|
-
},
|
|
3156
|
-
setCompactionSettings(input = {}) {
|
|
3157
|
-
const current = normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
|
|
3158
|
-
const next = { ...current };
|
|
3159
|
-
if (hasOwn(input, 'auto')) next.auto = input.auto !== false;
|
|
3160
|
-
if (hasOwn(input, 'enabled')) next.auto = input.enabled !== false;
|
|
3161
|
-
if (hasOwn(input, 'type') || hasOwn(input, 'compactType') || hasOwn(input, 'compact_type')) {
|
|
3162
|
-
const requestedType = input.type ?? input.compactType ?? input.compact_type;
|
|
3163
|
-
const compactType = normalizeCompactTypeSetting(requestedType, current.compactType || current.type || 'semantic');
|
|
3164
|
-
if (compactType === 'recall-fasttrack' && !memoryEnabled()) {
|
|
3165
|
-
throw new Error('recall-fasttrack compact requires memory to be enabled');
|
|
3166
|
-
}
|
|
3167
|
-
next.type = compactType;
|
|
3168
|
-
next.compactType = compactType;
|
|
3169
|
-
}
|
|
3170
|
-
const nextConfig = { ...config };
|
|
3171
|
-
nextConfig.compaction = normalizeCompactionConfig(next, { memoryEnabled: memoryEnabled() });
|
|
3172
|
-
saveConfigAndAdopt(nextConfig);
|
|
3173
|
-
if (session) {
|
|
3174
|
-
session.compaction = {
|
|
3175
|
-
...(session.compaction || {}),
|
|
3176
|
-
...normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() }),
|
|
3177
|
-
};
|
|
3178
|
-
}
|
|
3179
|
-
invalidateContextStatusCache();
|
|
3180
|
-
return normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
|
|
3181
|
-
},
|
|
3182
|
-
getMemorySettings() {
|
|
3183
|
-
return {
|
|
3184
|
-
enabled: memoryEnabled(),
|
|
3185
|
-
compactFastTrackAvailable: memoryEnabled(),
|
|
3186
|
-
};
|
|
3187
|
-
},
|
|
3188
|
-
async setMemoryEnabled(enabled) {
|
|
3189
|
-
const nextConfig = setModuleEnabledInConfig({ ...config }, 'memory', enabled !== false);
|
|
3190
|
-
if (enabled === false) {
|
|
3191
|
-
nextConfig.compaction = normalizeCompactionConfig(nextConfig.compaction, { memoryEnabled: false });
|
|
3192
|
-
}
|
|
3193
|
-
saveConfigAndAdopt(nextConfig);
|
|
3194
|
-
if (!memoryEnabled() && memoryModPromise) {
|
|
3195
|
-
await memoryModPromise.then((mod) => mod?.stop?.()).catch(() => {});
|
|
3196
|
-
memoryModPromise = null;
|
|
3197
|
-
}
|
|
3198
|
-
if (session && config.compaction) {
|
|
3199
|
-
session.compaction = {
|
|
3200
|
-
...(session.compaction || {}),
|
|
3201
|
-
...normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() }),
|
|
3202
|
-
};
|
|
3203
|
-
}
|
|
3204
|
-
invalidatePreSessionToolSurface();
|
|
3205
|
-
invalidateContextStatusCache();
|
|
3206
|
-
return this.getMemorySettings();
|
|
3207
|
-
},
|
|
3208
|
-
getChannelSettings(options = {}) {
|
|
3209
|
-
return {
|
|
3210
|
-
enabled: channelsEnabled(),
|
|
3211
|
-
...(options?.includeStatus === false ? {} : { status: channels.status() }),
|
|
3212
|
-
};
|
|
3213
|
-
},
|
|
3214
|
-
async setChannelsEnabled(enabled) {
|
|
3215
|
-
const nextConfig = setModuleEnabledInConfig({ ...config }, 'channels', enabled !== false);
|
|
3216
|
-
saveConfigAndAdopt(nextConfig);
|
|
3217
|
-
if (!channelsEnabled()) {
|
|
3218
|
-
if (channelStartTimer) {
|
|
3219
|
-
clearTimeout(channelStartTimer);
|
|
3220
|
-
channelStartTimer = null;
|
|
3221
|
-
}
|
|
3222
|
-
await channels.stop('settings-disabled', { waitForExit: false }).catch(() => {});
|
|
3223
|
-
} else {
|
|
3224
|
-
// Enabling channels in settings only boots the worker when this session
|
|
3225
|
-
// is in remote mode; otherwise the toggle just persists config.
|
|
3226
|
-
if (remoteEnabled) scheduleChannelStart(0);
|
|
3227
|
-
}
|
|
3228
|
-
invalidatePreSessionToolSurface();
|
|
3229
|
-
return this.getChannelSettings();
|
|
3230
|
-
},
|
|
3231
|
-
getSystemShell() {
|
|
3232
|
-
return normalizeSystemShellConfig(config.shell);
|
|
3233
|
-
},
|
|
3234
|
-
setSystemShell(input = {}) {
|
|
3235
|
-
const command = normalizeSystemShellCommand(typeof input === 'string' ? input : input?.command);
|
|
3236
|
-
saveConfigAndAdopt({
|
|
3237
|
-
...config,
|
|
3238
|
-
shell: command ? { ...(config.shell || {}), command } : {},
|
|
3239
|
-
});
|
|
3240
|
-
setConfiguredShell(command);
|
|
3241
|
-
return normalizeSystemShellConfig(config.shell);
|
|
3242
|
-
},
|
|
3243
|
-
setAutoClear(input = {}) {
|
|
3244
|
-
const current = normalizeAutoClearConfig(config.autoClear);
|
|
3245
|
-
const next = { ...current };
|
|
3246
|
-
if (hasOwn(input, 'enabled')) next.enabled = input.enabled !== false;
|
|
3247
|
-
if (hasOwn(input, 'idleMs')) {
|
|
3248
|
-
const idleMs = Number(input.idleMs);
|
|
3249
|
-
if (!Number.isFinite(idleMs) || idleMs <= 0) throw new Error('autoclear idleMs must be a positive number');
|
|
3250
|
-
next.idleMs = Math.max(60_000, Math.round(idleMs));
|
|
3251
|
-
}
|
|
3252
|
-
if (hasOwn(input, 'duration')) {
|
|
3253
|
-
const idleMs = parseDurationMs(input.duration);
|
|
3254
|
-
if (!idleMs) throw new Error('usage: /autoclear [on|off|status|<minutes|1h|90m>]');
|
|
3255
|
-
next.idleMs = idleMs;
|
|
3256
|
-
if (!hasOwn(input, 'enabled')) next.enabled = true;
|
|
3257
|
-
}
|
|
3258
|
-
saveConfigAndAdopt({ ...config, autoClear: next });
|
|
3259
|
-
return { ...normalizeAutoClearConfig(config.autoClear), label: formatDurationMs(normalizeAutoClearConfig(config.autoClear).idleMs) };
|
|
3260
|
-
},
|
|
3261
|
-
getUpdateSettings() {
|
|
3262
|
-
return {
|
|
3263
|
-
autoUpdate: autoUpdateEnabled(),
|
|
3264
|
-
currentVersion: updateCheckState.currentVersion || localPackageVersion(),
|
|
3265
|
-
latestVersion: updateCheckState.latestVersion,
|
|
3266
|
-
updateAvailable: updateCheckState.updateAvailable,
|
|
3267
|
-
lastCheckedAt: updateCheckState.lastCheckedAt,
|
|
3268
|
-
};
|
|
3269
|
-
},
|
|
3270
|
-
setAutoUpdate(enabled) {
|
|
3271
|
-
saveConfigAndAdopt({
|
|
3272
|
-
...config,
|
|
3273
|
-
update: { ...(config.update || {}), auto: enabled === true },
|
|
3274
|
-
});
|
|
3275
|
-
return this.getUpdateSettings();
|
|
3276
|
-
},
|
|
3277
|
-
async checkForUpdate(options = {}) {
|
|
3278
|
-
await checkForUpdateInternal({ force: options?.force === true });
|
|
3279
|
-
return this.getUpdateSettings();
|
|
3280
|
-
},
|
|
3281
|
-
async runUpdateNow() {
|
|
3282
|
-
const state = await runUpdateNowInternal();
|
|
3283
|
-
return { ok: state.phase === 'installed', ...state };
|
|
3284
|
-
},
|
|
3285
|
-
getUpdateStatus() {
|
|
3286
|
-
return { ...updateProcessState };
|
|
1879
|
+
return await getUsageDashboard(options);
|
|
3287
1880
|
},
|
|
3288
1881
|
async completeOnboarding(payload = {}) {
|
|
3289
1882
|
// Only fall back to the live runtime route when the caller actually sent a
|
|
@@ -3408,60 +2001,16 @@ function parsedProviderModelVersion(id) {
|
|
|
3408
2001
|
isRemoteEnabled() {
|
|
3409
2002
|
return isRemoteEnabled();
|
|
3410
2003
|
},
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
reloadChannelsSoon();
|
|
3419
|
-
return result;
|
|
3420
|
-
},
|
|
3421
|
-
saveTelegramToken(token) {
|
|
3422
|
-
const result = saveTelegramToken(token);
|
|
3423
|
-
reloadChannelsSoon();
|
|
3424
|
-
return result;
|
|
3425
|
-
},
|
|
3426
|
-
forgetTelegramToken() {
|
|
3427
|
-
const result = forgetTelegramToken();
|
|
3428
|
-
reloadChannelsSoon();
|
|
3429
|
-
return result;
|
|
3430
|
-
},
|
|
3431
|
-
setBackend(name) {
|
|
3432
|
-
// Validate synchronously (same contract as before: bad input throws on
|
|
3433
|
-
// this call so the TUI's try/catch can react immediately). The actual
|
|
3434
|
-
// channels-section file read-modify-write is the hitch source on the
|
|
3435
|
-
// settings-toggle key handler, so defer it through the same debounce
|
|
3436
|
-
// pattern as saveConfigAndAdopt/flushConfigSave instead of writing to
|
|
3437
|
-
// disk synchronously inside the key handler.
|
|
3438
|
-
const value = String(name || '').trim();
|
|
3439
|
-
if (value !== 'discord' && value !== 'telegram') {
|
|
3440
|
-
throw new Error('backend must be discord or telegram');
|
|
3441
|
-
}
|
|
3442
|
-
pendingBackendName = value;
|
|
3443
|
-
if (backendSaveTimer) clearTimeout(backendSaveTimer);
|
|
3444
|
-
backendSaveTimer = setTimeout(flushBackendSave, CONFIG_SAVE_DEBOUNCE_MS);
|
|
3445
|
-
backendSaveTimer.unref?.();
|
|
3446
|
-
return { ok: true, backend: value };
|
|
3447
|
-
},
|
|
3448
|
-
saveWebhookAuthtoken(token) {
|
|
3449
|
-
const result = saveWebhookAuthtoken(token);
|
|
3450
|
-
reloadChannelsSoon();
|
|
3451
|
-
return result;
|
|
3452
|
-
},
|
|
3453
|
-
forgetWebhookAuthtoken() {
|
|
3454
|
-
const result = forgetWebhookAuthtoken();
|
|
3455
|
-
reloadChannelsSoon();
|
|
3456
|
-
return result;
|
|
3457
|
-
},
|
|
3458
|
-
saveChannel(entry) {
|
|
3459
|
-
const result = saveChannel(entry);
|
|
3460
|
-
reloadChannelsSoon();
|
|
3461
|
-
return result;
|
|
2004
|
+
// Subscribe to non-user-initiated remote flips (seat superseded). Returns
|
|
2005
|
+
// an unsubscribe function. TUI uses this to sync its Remote indicator and
|
|
2006
|
+
// show a "remote taken over" notice.
|
|
2007
|
+
onRemoteStateChange(listener) {
|
|
2008
|
+
if (typeof listener !== 'function') return () => {};
|
|
2009
|
+
remoteStateListeners.add(listener);
|
|
2010
|
+
return () => remoteStateListeners.delete(listener);
|
|
3462
2011
|
},
|
|
3463
|
-
|
|
3464
|
-
const result =
|
|
2012
|
+
setChannel(entry) {
|
|
2013
|
+
const result = setChannel(entry);
|
|
3465
2014
|
reloadChannelsSoon();
|
|
3466
2015
|
return result;
|
|
3467
2016
|
},
|
|
@@ -3685,18 +2234,13 @@ function parsedProviderModelVersion(id) {
|
|
|
3685
2234
|
nextConfig.agent = agent;
|
|
3686
2235
|
}
|
|
3687
2236
|
adoptConfig(nextConfig);
|
|
3688
|
-
|
|
3689
|
-
if (outputStyleSaveTimer) clearTimeout(outputStyleSaveTimer);
|
|
3690
|
-
outputStyleSaveTimer = setTimeout(flushOutputStyleSave, CONFIG_SAVE_DEBOUNCE_MS);
|
|
3691
|
-
outputStyleSaveTimer.unref?.();
|
|
2237
|
+
scheduleOutputStyleSave(selected.id);
|
|
3692
2238
|
// Reuse the catalog already scanned above for `before` instead of a
|
|
3693
2239
|
// second forced-fresh filesystem scan; refresh the status cache
|
|
3694
2240
|
// in-memory (short TTL, same as getOutputStyleStatusCached) so reads
|
|
3695
2241
|
// during the debounce window see the just-selected style.
|
|
3696
2242
|
const freshStatus = { configured: selected.id, current: selected, styles: before.styles };
|
|
3697
|
-
|
|
3698
|
-
outputStyleStatusCacheAt = performance.now();
|
|
3699
|
-
outputStyleStatusCacheDir = resolve(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR);
|
|
2243
|
+
seedOutputStyleStatusCache(freshStatus);
|
|
3700
2244
|
const hasConversation = sessionHasConversationMessages(session);
|
|
3701
2245
|
let appliedToCurrentSession = !hasConversation;
|
|
3702
2246
|
if (session?.id && !hasConversation) {
|
|
@@ -3791,25 +2335,28 @@ function parsedProviderModelVersion(id) {
|
|
|
3791
2335
|
// JSONL and pushes the surface view to Discord. Gated on remoteEnabled
|
|
3792
2336
|
// so non-remote sessions write nothing.
|
|
3793
2337
|
if (remoteEnabled) {
|
|
3794
|
-
const twKey = `${session.id}\u0000${currentCwd}`;
|
|
3795
2338
|
// Reset per-turn dedup tracker so an identical answer in a later turn
|
|
3796
2339
|
// is still written (the guard only prevents same-turn double-write).
|
|
3797
2340
|
_lastAppendedAssistant = '';
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
2341
|
+
const prevKey = _twKey;
|
|
2342
|
+
ensureRemoteTranscriptWriter();
|
|
2343
|
+
// The writer moved to a NEW session/cwd (e.g. lazy first-turn
|
|
2344
|
+
// session create after /remote, or /new mid-remote). Re-pin the
|
|
2345
|
+
// channel worker's output forwarder onto the fresh transcript NOW —
|
|
2346
|
+
// otherwise it keeps tailing the previous binding until an inbound
|
|
2347
|
+
// Discord message triggers the steal path, so terminal-initiated
|
|
2348
|
+
// turns would never forward. activate_channel_bridge is idempotent
|
|
2349
|
+
// force-takeover (claim + rebind); fire-and-forget so the turn is
|
|
2350
|
+
// never blocked on worker IPC.
|
|
2351
|
+
if (_twKey && _twKey !== prevKey && channelsEnabled() && !envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
2352
|
+
void invokeChannelStart()
|
|
2353
|
+
.then(() => {
|
|
2354
|
+
// stopRemote()/superseded/close may have landed while the
|
|
2355
|
+
// worker was starting — never re-claim for a turned-off session.
|
|
2356
|
+
if (!remoteEnabled || closeRequested) return undefined;
|
|
2357
|
+
return channels.execute('activate_channel_bridge', { active: true });
|
|
2358
|
+
})
|
|
2359
|
+
.catch((error) => bootProfile('channels:turn-rebind-failed', { error: error?.message || String(error) }));
|
|
3813
2360
|
}
|
|
3814
2361
|
}
|
|
3815
2362
|
hooks.emit('turn:start', { sessionId: session.id, prompt, cwd: currentCwd });
|
|
@@ -4037,7 +2584,7 @@ function parsedProviderModelVersion(id) {
|
|
|
4037
2584
|
return { ...result, status: this.toolsStatus() };
|
|
4038
2585
|
},
|
|
4039
2586
|
setCwd(path) {
|
|
4040
|
-
applyResolvedCwd(resolveCwdPath(
|
|
2587
|
+
applyResolvedCwd(resolveCwdPath(path));
|
|
4041
2588
|
return currentCwd;
|
|
4042
2589
|
},
|
|
4043
2590
|
mcpStatus() {
|
|
@@ -4248,9 +2795,19 @@ function parsedProviderModelVersion(id) {
|
|
|
4248
2795
|
...(args || {}),
|
|
4249
2796
|
query: baseQuery,
|
|
4250
2797
|
cwd: args?.cwd || currentCwd,
|
|
2798
|
+
// Grouping hint for multi-session browse output: lets the memory
|
|
2799
|
+
// service mark THIS session's group as "(current)". Not a filter.
|
|
2800
|
+
...(session?.id ? { currentSessionId: session.id } : {}),
|
|
4251
2801
|
};
|
|
4252
2802
|
let result = '(no results)';
|
|
4253
2803
|
if (session?.id && args?.currentSession !== false && args?.forceCycleOnEmpty !== false) {
|
|
2804
|
+
// Empty-fallback: hydrate the current transcript into the memory DB
|
|
2805
|
+
// (no LLM — ingest only) and re-search session-scoped with the raw
|
|
2806
|
+
// leg on. The old path also ran a synchronous cycle1 (LLM chunking)
|
|
2807
|
+
// here, which made an empty recall the slowest possible recall and
|
|
2808
|
+
// called the LLM even with recap/memory off; the raw rows are
|
|
2809
|
+
// searchable directly (FTS/trgm + post-ingest raw embeddings), so
|
|
2810
|
+
// chunking is left to the background cycle.
|
|
4254
2811
|
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
4255
2812
|
if (messages.length > 0) {
|
|
4256
2813
|
await memoryMod.handleToolCall('memory', {
|
|
@@ -4259,12 +2816,6 @@ function parsedProviderModelVersion(id) {
|
|
|
4259
2816
|
cwd: currentCwd,
|
|
4260
2817
|
messages,
|
|
4261
2818
|
});
|
|
4262
|
-
await memoryMod.handleToolCall('memory', {
|
|
4263
|
-
action: 'cycle1',
|
|
4264
|
-
min_batch: 1,
|
|
4265
|
-
session_cap: 1,
|
|
4266
|
-
batch_size: Math.max(1, Math.min(100, messages.length)),
|
|
4267
|
-
});
|
|
4268
2819
|
result = toolResponseText(await memoryMod.handleToolCall('recall', {
|
|
4269
2820
|
...baseArgs,
|
|
4270
2821
|
sessionId: session.id,
|
|
@@ -4417,37 +2968,30 @@ function parsedProviderModelVersion(id) {
|
|
|
4417
2968
|
try { flushConfigSave(); } catch {}
|
|
4418
2969
|
try { flushBackendSave(); } catch {}
|
|
4419
2970
|
try { flushOutputStyleSave(); } catch {}
|
|
4420
|
-
if (channelStartTimer) {
|
|
4421
|
-
clearTimeout(channelStartTimer);
|
|
4422
|
-
channelStartTimer = null;
|
|
4423
|
-
}
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
providerModelWarmupTimer = null;
|
|
4435
|
-
}
|
|
4436
|
-
if (modelCatalogWarmupTimer) {
|
|
4437
|
-
clearTimeout(modelCatalogWarmupTimer);
|
|
4438
|
-
modelCatalogWarmupTimer = null;
|
|
4439
|
-
}
|
|
4440
|
-
if (codeGraphPrewarmTimer) {
|
|
4441
|
-
clearTimeout(codeGraphPrewarmTimer);
|
|
4442
|
-
codeGraphPrewarmTimer = null;
|
|
2971
|
+
if (prewarmTimers.channelStartTimer) {
|
|
2972
|
+
clearTimeout(prewarmTimers.channelStartTimer);
|
|
2973
|
+
prewarmTimers.channelStartTimer = null;
|
|
2974
|
+
}
|
|
2975
|
+
for (const timerKey of [
|
|
2976
|
+
'providerSetupWarmupTimer',
|
|
2977
|
+
'providerWarmupTimer',
|
|
2978
|
+
'providerModelWarmupTimer',
|
|
2979
|
+
'modelCatalogWarmupTimer',
|
|
2980
|
+
]) {
|
|
2981
|
+
if (warmupTimers[timerKey]) {
|
|
2982
|
+
clearTimeout(warmupTimers[timerKey]);
|
|
2983
|
+
warmupTimers[timerKey] = null;
|
|
2984
|
+
}
|
|
4443
2985
|
}
|
|
4444
|
-
if (
|
|
4445
|
-
clearTimeout(
|
|
4446
|
-
|
|
2986
|
+
if (prewarmTimers.codeGraphPrewarmTimer) {
|
|
2987
|
+
clearTimeout(prewarmTimers.codeGraphPrewarmTimer);
|
|
2988
|
+
prewarmTimers.codeGraphPrewarmTimer = null;
|
|
4447
2989
|
}
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
2990
|
+
for (const timerKey of ['statuslineUsageWarmupTimer', 'statuslineUsageRefreshTimer']) {
|
|
2991
|
+
if (warmupTimers[timerKey]) {
|
|
2992
|
+
clearTimeout(warmupTimers[timerKey]);
|
|
2993
|
+
warmupTimers[timerKey] = null;
|
|
2994
|
+
}
|
|
4451
2995
|
}
|
|
4452
2996
|
try { cancelBackgroundTasks({ reason, notify: false }); } catch {}
|
|
4453
2997
|
const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
|
|
@@ -4471,7 +3015,19 @@ function parsedProviderModelVersion(id) {
|
|
|
4471
3015
|
let ok = false;
|
|
4472
3016
|
if (session?.id) {
|
|
4473
3017
|
statusRoutes?.clearGatewaySessionRoute?.(session.id);
|
|
4474
|
-
|
|
3018
|
+
// Bug fix: runtime stop/exit (TUI Ctrl-C, process exit) previously
|
|
3019
|
+
// always tombstoned the current session, so a session you were
|
|
3020
|
+
// mid-conversation in vanished from the Resume list the instant you
|
|
3021
|
+
// quit and was hard-deleted by the 24h tombstone sweep. Only
|
|
3022
|
+
// tombstone truly-empty scratch sessions; non-empty sessions must
|
|
3023
|
+
// survive exit resumable.
|
|
3024
|
+
// liveTurnMessages holds the in-flight user prompt until turn
|
|
3025
|
+
// commit — an active first-turn ask has its user message there,
|
|
3026
|
+
// not yet in session.messages, so it must also be checked or a
|
|
3027
|
+
// first-turn exit could still burn a real session.
|
|
3028
|
+
const tombstone = !hasUserConversationMessage(session.messages)
|
|
3029
|
+
&& !hasUserConversationMessage(session.liveTurnMessages);
|
|
3030
|
+
ok = mgr.closeSession(session.id, reason, { tombstone });
|
|
4475
3031
|
session = null;
|
|
4476
3032
|
}
|
|
4477
3033
|
const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
|
|
@@ -4518,7 +3074,10 @@ function parsedProviderModelVersion(id) {
|
|
|
4518
3074
|
const agent = clean(s.agent || '').toLowerCase();
|
|
4519
3075
|
const leadish = agent === 'lead'
|
|
4520
3076
|
|| sourceType === 'lead'
|
|
4521
|
-
|
|
3077
|
+
// Bug fix: side-terminal cli sessions have a non-empty/non-'main' sourceName
|
|
3078
|
+
// (e.g. terminal id) and were being hidden from resume even though they are
|
|
3079
|
+
// legitimate user sessions, not agent-owned. Any sourceName is fine for cli.
|
|
3080
|
+
|| (sourceType === 'cli')
|
|
4522
3081
|
|| (!sourceType && !sourceName && !isAgentOwner(owner));
|
|
4523
3082
|
if (!leadish) return null;
|
|
4524
3083
|
let preview = cleanSessionPreview(s.preview || '');
|
|
@@ -4532,7 +3091,12 @@ function parsedProviderModelVersion(id) {
|
|
|
4532
3091
|
preview = userPreviews[userPreviews.length - 1] || userPreviews[0] || '';
|
|
4533
3092
|
messageCount = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant')).length;
|
|
4534
3093
|
}
|
|
4535
|
-
|
|
3094
|
+
// Bug fix: sessions whose preview couldn't be derived (e.g. noise-only user
|
|
3095
|
+
// turns) were silently dropped from the resume list even when they had real
|
|
3096
|
+
// messages. Keep the row and let preview fall back to '' (TUI renders
|
|
3097
|
+
// '(no message)' for empty preview); only drop truly-empty scratch sessions
|
|
3098
|
+
// with zero visible messages.
|
|
3099
|
+
if (!preview && messageCount === 0) return null;
|
|
4536
3100
|
return {
|
|
4537
3101
|
id: s.id,
|
|
4538
3102
|
updatedAt: s.updatedAt,
|
|
@@ -4545,17 +3109,44 @@ function parsedProviderModelVersion(id) {
|
|
|
4545
3109
|
}).filter(Boolean);
|
|
4546
3110
|
},
|
|
4547
3111
|
async newSession() {
|
|
4548
|
-
if (session?.id)
|
|
3112
|
+
if (session?.id) {
|
|
3113
|
+
// Bug fix: /new used to unconditionally tombstone the outgoing
|
|
3114
|
+
// session, so switching to a fresh session burned whatever you'd
|
|
3115
|
+
// been working on — it dropped off the Resume list immediately and
|
|
3116
|
+
// was hard-deleted after the 24h tombstone sweep. Only tombstone
|
|
3117
|
+
// truly-empty scratch sessions; keep non-empty ones resumable.
|
|
3118
|
+
const tombstone = !hasUserConversationMessage(session.messages)
|
|
3119
|
+
&& !hasUserConversationMessage(session.liveTurnMessages);
|
|
3120
|
+
mgr.closeSession(session.id, 'cli-new', { tombstone });
|
|
3121
|
+
// Bug fix: closeSession({tombstone:false}) keeps the outgoing
|
|
3122
|
+
// session file intact so it stays resumable — but that meant
|
|
3123
|
+
// createCurrentSession()'s live-session reuse check (session?.id →
|
|
3124
|
+
// mgr.getSession(...) not closed) happily reloaded the SAME session,
|
|
3125
|
+
// so /new never actually reset the transcript and the context gauge
|
|
3126
|
+
// never returned to 0. Drop the in-memory reference so a fresh
|
|
3127
|
+
// session is always created.
|
|
3128
|
+
session = null;
|
|
3129
|
+
}
|
|
3130
|
+
invalidateContextStatusCache();
|
|
4549
3131
|
await createCurrentSession();
|
|
4550
3132
|
return session.id;
|
|
4551
3133
|
},
|
|
4552
3134
|
async resume(id) {
|
|
4553
3135
|
const previousId = session?.id || null;
|
|
3136
|
+
const previousMessages = session?.messages || null;
|
|
3137
|
+
const previousLive = session?.liveTurnMessages || null;
|
|
4554
3138
|
const resumed = await mgr.resumeSession(id, toolSpecForMode(mode));
|
|
4555
3139
|
if (!resumed) return null;
|
|
4556
3140
|
if (previousId && previousId !== resumed.id) {
|
|
4557
3141
|
statusRoutes?.clearGatewaySessionRoute?.(previousId);
|
|
4558
|
-
|
|
3142
|
+
// Bug fix: /resume used to unconditionally tombstone the session
|
|
3143
|
+
// you were switching away from, so it vanished from the Resume
|
|
3144
|
+
// list right away and was hard-deleted after the 24h sweep — the
|
|
3145
|
+
// exact "burn a resumable session" bug this fix targets. Only
|
|
3146
|
+
// tombstone truly-empty scratch sessions.
|
|
3147
|
+
const tombstone = !hasUserConversationMessage(previousMessages)
|
|
3148
|
+
&& !hasUserConversationMessage(previousLive);
|
|
3149
|
+
mgr.closeSession(previousId, 'cli-resume', { tombstone });
|
|
4559
3150
|
}
|
|
4560
3151
|
session = resumed;
|
|
4561
3152
|
currentCwd = resumed.cwd || currentCwd;
|