mixdog 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -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/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-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 +215 -29
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- 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-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +23 -63
- 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 +6 -5
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/worker/AGENT.md +8 -4
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +632 -2042
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +0 -11
- 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 +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -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 +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -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/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 +182 -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 +251 -397
- 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/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 +32 -41
- 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 +12 -292
- package/src/runtime/channels/index.mjs +229 -663
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- 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/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 +312 -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 +36 -570
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +201 -1948
- 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/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 +22 -2
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +780 -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/launcher-control.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 +84 -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 +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +319 -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/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 +110 -671
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +30 -9
- 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 +945 -8094
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -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 +324 -0
- package/src/tui/app/model-options.mjs +330 -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 +490 -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 +671 -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 +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +23 -101
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- 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 +19652 -18630
- 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 +122 -562
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- 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 +67 -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 +1 -1
- package/src/workflows/sequential/WORKFLOW.md +1 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- 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/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,
|
|
@@ -60,7 +60,6 @@ import {
|
|
|
60
60
|
forgetDiscordToken,
|
|
61
61
|
forgetTelegramToken,
|
|
62
62
|
forgetWebhookAuthtoken,
|
|
63
|
-
renderChannelStatus,
|
|
64
63
|
saveChannel,
|
|
65
64
|
saveDiscordToken,
|
|
66
65
|
saveTelegramToken,
|
|
@@ -137,9 +136,13 @@ import {
|
|
|
137
136
|
normalizeSystemShellConfig,
|
|
138
137
|
normalizeSystemShellCommand,
|
|
139
138
|
normalizeAutoClearConfig,
|
|
139
|
+
resolveAutoClearIdleMs,
|
|
140
|
+
autoClearIdleMsForProvider,
|
|
140
141
|
normalizeCompactionConfig,
|
|
141
142
|
moduleEnabled,
|
|
142
143
|
setModuleEnabledInConfig,
|
|
144
|
+
recapEnabled,
|
|
145
|
+
setRecapEnabledInConfig,
|
|
143
146
|
formatDurationMs,
|
|
144
147
|
parseDurationMs,
|
|
145
148
|
modelMetaLooksResolved,
|
|
@@ -208,6 +211,36 @@ import {
|
|
|
208
211
|
// Re-exported for external consumers (scripts/tool-smoke.mjs) that imported
|
|
209
212
|
// these from this module before the tool-catalog extraction.
|
|
210
213
|
export { defaultDeferredToolNames, compactToolSearchDescription } from './session-runtime/tool-catalog.mjs';
|
|
214
|
+
import {
|
|
215
|
+
TOOL_SEARCH_TOOL,
|
|
216
|
+
CWD_TOOL,
|
|
217
|
+
SKILL_TOOL,
|
|
218
|
+
LEAD_DISALLOWED_TOOLS,
|
|
219
|
+
applyStandaloneToolDefaults,
|
|
220
|
+
} from './session-runtime/tool-defs.mjs';
|
|
221
|
+
import { ONBOARDING_VERSION, QUICK_SEARCH_MODELS } from './session-runtime/quick-search-models.mjs';
|
|
222
|
+
import {
|
|
223
|
+
sortProviderModels as sortProviderModelsRaw,
|
|
224
|
+
providerModelCacheRow as providerModelCacheRowRaw,
|
|
225
|
+
} from './session-runtime/model-recency.mjs';
|
|
226
|
+
import { createNativeSearch } from './session-runtime/native-search.mjs';
|
|
227
|
+
import { createConfigLifecycle } from './session-runtime/config-lifecycle.mjs';
|
|
228
|
+
import { attachSessionHooks } from './session-runtime/session-hooks.mjs';
|
|
229
|
+
import { createQuickModelRows } from './session-runtime/quick-model-rows.mjs';
|
|
230
|
+
import { createWarmupSchedulers } from './session-runtime/warmup-schedulers.mjs';
|
|
231
|
+
import { createPrewarmSchedulers } from './session-runtime/prewarm.mjs';
|
|
232
|
+
import { createMcpGlue } from './session-runtime/mcp-glue.mjs';
|
|
233
|
+
import { createCwdPlugins } from './session-runtime/cwd-plugins.mjs';
|
|
234
|
+
import { createSettingsApi } from './session-runtime/settings-api.mjs';
|
|
235
|
+
import { createProviderModels } from './session-runtime/provider-models.mjs';
|
|
236
|
+
import { createProviderUsage } from './session-runtime/provider-usage.mjs';
|
|
237
|
+
// Re-exported for external consumers (scripts/tool-smoke.mjs) that imported
|
|
238
|
+
// these from this module before the tool-defs extraction.
|
|
239
|
+
export { TOOL_SEARCH_TOOL, SKILL_TOOL };
|
|
240
|
+
// Back-compat test alias; delegates to the extracted helper.
|
|
241
|
+
export function __applyStandaloneToolDefaultsForTest(tool) {
|
|
242
|
+
return applyStandaloneToolDefaults(tool);
|
|
243
|
+
}
|
|
211
244
|
|
|
212
245
|
const RUNTIME = './runtime/agent/orchestrator';
|
|
213
246
|
const SEARCH_RUNTIME = './runtime/search/index.mjs';
|
|
@@ -276,146 +309,7 @@ async function profiledImport(label, spec, { optional = false } = {}) {
|
|
|
276
309
|
throw error;
|
|
277
310
|
}
|
|
278
311
|
}
|
|
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
312
|
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
313
|
// Workflow/agent pack loaders bound to this runtime's root/data layout.
|
|
420
314
|
const {
|
|
421
315
|
listWorkflowPacks,
|
|
@@ -445,15 +339,6 @@ export function __saveModelSettingsForTest(cfgMod, route, options = {}) {
|
|
|
445
339
|
return saveModelSettings(cfgMod, route, options);
|
|
446
340
|
}
|
|
447
341
|
|
|
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
342
|
export async function createMixdogSessionRuntime({
|
|
458
343
|
provider,
|
|
459
344
|
model,
|
|
@@ -516,37 +401,16 @@ export async function createMixdogSessionRuntime({
|
|
|
516
401
|
let memoryModPromise = null;
|
|
517
402
|
let searchModPromise = null;
|
|
518
403
|
let codeGraphModPromise = null;
|
|
519
|
-
let outputStyleStatusCache = null;
|
|
520
|
-
let outputStyleStatusCacheAt = 0;
|
|
521
|
-
let outputStyleStatusCacheDir = '';
|
|
522
404
|
|
|
523
|
-
|
|
405
|
+
// Memory module is always-on. `memoryEnabled()` is kept as a thin alias that
|
|
406
|
+
// now always returns true (callers/compaction helpers still reference it);
|
|
407
|
+
// the user-facing toggle is `recap` (background cycles only), read via
|
|
408
|
+
// recapEnabled(config).
|
|
409
|
+
const memoryEnabled = () => true;
|
|
410
|
+
const recapEnabledFn = () => recapEnabled(config, true);
|
|
524
411
|
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
412
|
|
|
548
413
|
async function getMemoryModule() {
|
|
549
|
-
if (!memoryEnabled()) throw new Error('memory is disabled in settings');
|
|
550
414
|
const startedAt = performance.now();
|
|
551
415
|
memoryModPromise ??= Promise.resolve(memoryRuntime);
|
|
552
416
|
const mod = await memoryModPromise;
|
|
@@ -565,211 +429,6 @@ export async function createMixdogSessionRuntime({
|
|
|
565
429
|
return mod;
|
|
566
430
|
}
|
|
567
431
|
|
|
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
432
|
async function getCodeGraphModule() {
|
|
774
433
|
const startedAt = performance.now();
|
|
775
434
|
codeGraphModPromise ??= import(CODE_GRAPH_RUNTIME);
|
|
@@ -808,60 +467,6 @@ export async function createMixdogSessionRuntime({
|
|
|
808
467
|
}
|
|
809
468
|
}
|
|
810
469
|
|
|
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
470
|
const configStartedAt = performance.now();
|
|
866
471
|
let config = cfgMod.loadConfig({ secrets: false });
|
|
867
472
|
setConfiguredShell(normalizeSystemShellConfig(config.shell).command);
|
|
@@ -875,17 +480,26 @@ export async function createMixdogSessionRuntime({
|
|
|
875
480
|
let currentCwd = cwd;
|
|
876
481
|
let sessionNeedsCwdRefresh = false;
|
|
877
482
|
let closeRequested = false;
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
483
|
+
const warmupTimers = {
|
|
484
|
+
providerSetupWarmupTimer: null,
|
|
485
|
+
providerWarmupTimer: null,
|
|
486
|
+
providerModelWarmupTimer: null,
|
|
487
|
+
modelCatalogWarmupTimer: null,
|
|
488
|
+
statuslineUsageWarmupTimer: null,
|
|
489
|
+
statuslineUsageRefreshTimer: null,
|
|
490
|
+
};
|
|
491
|
+
// Prewarm/channel-start timer handles + async state, owned here so the
|
|
492
|
+
// teardown clearTimeout sweep still sees them; the prewarm scheduler factory
|
|
493
|
+
// mutates these objects in place (see createPrewarmSchedulers).
|
|
494
|
+
const prewarmTimers = {
|
|
495
|
+
codeGraphPrewarmTimer: null,
|
|
496
|
+
channelStartTimer: null,
|
|
497
|
+
};
|
|
498
|
+
const prewarmState = {
|
|
499
|
+
codeGraphPrewarmInFlight: false,
|
|
500
|
+
codeGraphPrewarmQueuedCwd: '',
|
|
501
|
+
channelStartPromise: null,
|
|
502
|
+
};
|
|
889
503
|
let activeTurnCount = 0;
|
|
890
504
|
let firstTurnCompleted = false;
|
|
891
505
|
function hookTranscriptPath(sessionId) {
|
|
@@ -955,20 +569,50 @@ export async function createMixdogSessionRuntime({
|
|
|
955
569
|
let codeGraphFirstTurnPrewarmDone = false;
|
|
956
570
|
const modelMetaByRoute = new Map();
|
|
957
571
|
const notificationListeners = new Set();
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
572
|
+
// Remote seat listeners (TUI): fired when remote mode flips outside a direct
|
|
573
|
+
// user action — currently only the superseded (seat stolen) path.
|
|
574
|
+
const remoteStateListeners = new Set();
|
|
575
|
+
function emitRemoteStateChange(enabled, reason = '') {
|
|
576
|
+
for (const listener of [...remoteStateListeners]) {
|
|
577
|
+
try { listener({ enabled: enabled === true, reason: String(reason || '') }); } catch {}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const providerModelCaches = {
|
|
581
|
+
providerModelsCache: { models: null, at: 0 },
|
|
582
|
+
providerModelsPromise: null,
|
|
583
|
+
providerModelsLoadSeq: 0,
|
|
584
|
+
searchProviderModelsCache: { models: null, at: 0 },
|
|
585
|
+
};
|
|
586
|
+
const providerUsageCaches = {
|
|
587
|
+
usageDashboardCache: { dashboard: null, at: 0 },
|
|
588
|
+
usageDashboardPromise: null,
|
|
589
|
+
providerSetupCache: { setup: null, at: 0 },
|
|
590
|
+
providerSetupQuickCache: { setup: null, at: 0 },
|
|
591
|
+
providerSetupPromise: null,
|
|
592
|
+
};
|
|
967
593
|
let providerInitPromise = null;
|
|
968
|
-
let mcpFailures = [];
|
|
969
594
|
let lastProjectMcpKey = null;
|
|
970
|
-
|
|
971
|
-
|
|
595
|
+
// MCP connect state, owned here so teardown/reconnect paths still observe it;
|
|
596
|
+
// the mcp-glue factory mutates this object in place (see createMcpGlue).
|
|
597
|
+
const mcpState = {
|
|
598
|
+
mcpFailures: [],
|
|
599
|
+
mcpConnectGeneration: 0,
|
|
600
|
+
mcpConnectInFlight: null,
|
|
601
|
+
};
|
|
602
|
+
// MCP glue factory — config/currentCwd live-bound; connect state shared via
|
|
603
|
+
// the caller-owned mcpState object above.
|
|
604
|
+
const {
|
|
605
|
+
mcpTransportLabel,
|
|
606
|
+
resolveEffectiveMcpServers,
|
|
607
|
+
mcpStatus,
|
|
608
|
+
connectConfiguredMcp,
|
|
609
|
+
normalizeMcpServerInput,
|
|
610
|
+
} = createMcpGlue({
|
|
611
|
+
mcpClient,
|
|
612
|
+
getConfig: () => config,
|
|
613
|
+
getCurrentCwd: () => currentCwd,
|
|
614
|
+
state: mcpState,
|
|
615
|
+
});
|
|
972
616
|
let preSessionToolSurface = null;
|
|
973
617
|
let contextStatusCacheKey = null;
|
|
974
618
|
let contextStatusCacheValue = null;
|
|
@@ -1041,9 +685,12 @@ export async function createMixdogSessionRuntime({
|
|
|
1041
685
|
// the user opts in via setAutoUpdate(true)); a failed check or install is
|
|
1042
686
|
// swallowed — getUpdateStatus()/getUpdateSettings() are the only surfaces,
|
|
1043
687
|
// there is no push notice channel from runtime -> TUI today.
|
|
688
|
+
// force:true — always hit the registry at boot (the 24h disk cache went
|
|
689
|
+
// stale-visible: it kept reporting an older "latest" than the installed
|
|
690
|
+
// version). checkLatestVersion() still falls back to the cache offline.
|
|
1044
691
|
const updateBootTimer = setTimeout(() => {
|
|
1045
692
|
void (async () => {
|
|
1046
|
-
await checkForUpdateInternal({ force:
|
|
693
|
+
await checkForUpdateInternal({ force: true });
|
|
1047
694
|
if (autoUpdateEnabled() && updateCheckState.updateAvailable) {
|
|
1048
695
|
await runUpdateNowInternal();
|
|
1049
696
|
}
|
|
@@ -1051,15 +698,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1051
698
|
}, 0);
|
|
1052
699
|
updateBootTimer.unref?.();
|
|
1053
700
|
|
|
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
701
|
function emitRuntimeNotification(content, meta = {}) {
|
|
1064
702
|
const text = String(content || '').trim();
|
|
1065
703
|
if (!text) return false;
|
|
@@ -1101,40 +739,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1101
739
|
};
|
|
1102
740
|
}
|
|
1103
741
|
|
|
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
742
|
function skillsStatus() {
|
|
1139
743
|
const skills = typeof contextMod.collectSkillsCached === 'function'
|
|
1140
744
|
? contextMod.collectSkillsCached(currentCwd)
|
|
@@ -1158,113 +762,51 @@ export async function createMixdogSessionRuntime({
|
|
|
1158
762
|
};
|
|
1159
763
|
}
|
|
1160
764
|
|
|
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
|
-
}
|
|
765
|
+
// cwd resolution/apply + plugins-status + core-memory context. Extracted to
|
|
766
|
+
// session-runtime/cwd-plugins.mjs; the facade keeps ownership of the mutable
|
|
767
|
+
// currentCwd/session/config/lastProjectMcpKey locals via getter/setter
|
|
768
|
+
// injection and passes the later-defined callbacks (prewarm/tool-surface/
|
|
769
|
+
// memory) as closures.
|
|
770
|
+
const {
|
|
771
|
+
resolveCwdPath,
|
|
772
|
+
applyResolvedCwd,
|
|
773
|
+
refreshSessionForCwdIfNeeded,
|
|
774
|
+
pluginsStatus,
|
|
775
|
+
loadCoreMemoryContext,
|
|
776
|
+
} = createCwdPlugins({
|
|
777
|
+
getCurrentCwd: () => currentCwd,
|
|
778
|
+
setCurrentCwd: (next) => { currentCwd = next; },
|
|
779
|
+
getConfig: () => config,
|
|
780
|
+
getSession: () => session,
|
|
781
|
+
getRoute: () => route,
|
|
782
|
+
getLastProjectMcpKey: () => lastProjectMcpKey,
|
|
783
|
+
setLastProjectMcpKey: (next) => { lastProjectMcpKey = next; },
|
|
784
|
+
isCodeGraphPrewarmLazy: () => codeGraphPrewarmLazy,
|
|
785
|
+
isCodeGraphFirstTurnPrewarmDone: () => codeGraphFirstTurnPrewarmDone,
|
|
786
|
+
getCodeGraphPrewarmDelayMs: () => codeGraphPrewarmDelayMs,
|
|
787
|
+
setSessionNeedsCwdRefresh: (next) => { sessionNeedsCwdRefresh = next; },
|
|
788
|
+
connectConfiguredMcp,
|
|
789
|
+
invalidatePreSessionToolSurface: (...a) => invalidatePreSessionToolSurface(...a),
|
|
790
|
+
scheduleCodeGraphPrewarm: (...a) => scheduleCodeGraphPrewarm(...a),
|
|
791
|
+
hooks,
|
|
792
|
+
hookCommonPayload: (...a) => hookCommonPayload(...a),
|
|
793
|
+
bootProfile,
|
|
794
|
+
getMemoryModule: (...a) => getMemoryModule(...a),
|
|
795
|
+
listRegisteredPlugins,
|
|
796
|
+
pluginAdminStatus,
|
|
797
|
+
pluginManifest,
|
|
798
|
+
pluginMcpServerName,
|
|
799
|
+
mcpScriptForPlugin,
|
|
800
|
+
countSkillFiles,
|
|
801
|
+
readProjectMcpServers,
|
|
802
|
+
writeLastSessionCwd,
|
|
803
|
+
clean,
|
|
804
|
+
resolve,
|
|
805
|
+
statSync,
|
|
806
|
+
existsSync,
|
|
807
|
+
cfgMod,
|
|
808
|
+
STANDALONE_DATA_DIR,
|
|
809
|
+
});
|
|
1268
810
|
|
|
1269
811
|
function skillContent(name) {
|
|
1270
812
|
const res = typeof contextMod.loadSkillResource === 'function'
|
|
@@ -1308,166 +850,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1308
850
|
return { name, filePath };
|
|
1309
851
|
}
|
|
1310
852
|
|
|
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
853
|
const agentToolStartedAt = performance.now();
|
|
1472
854
|
const agentTool = createStandaloneAgent({
|
|
1473
855
|
cfgMod,
|
|
@@ -1508,6 +890,16 @@ export async function createMixdogSessionRuntime({
|
|
|
1508
890
|
dataDir: cfgMod.getPluginData(),
|
|
1509
891
|
cwd,
|
|
1510
892
|
onNotify: (msg) => {
|
|
893
|
+
// Single-holder remote: the worker reports it lost the bridge seat to a
|
|
894
|
+
// newer remote session. Drop remote mode entirely on this session (no
|
|
895
|
+
// handover, no retry) and tell UI listeners so the indicator updates.
|
|
896
|
+
if (msg?.method === 'notifications/mixdog/remote') {
|
|
897
|
+
if (msg?.params?.state === 'superseded' && remoteEnabled) {
|
|
898
|
+
stopRemote('superseded-by-newer-remote-session');
|
|
899
|
+
emitRemoteStateChange(false, 'superseded');
|
|
900
|
+
}
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
1511
903
|
if (msg?.method !== 'notifications/claude/channel') return;
|
|
1512
904
|
const params = msg?.params && typeof msg.params === 'object' ? msg.params : {};
|
|
1513
905
|
const meta = params.meta && typeof params.meta === 'object' ? params.meta : {};
|
|
@@ -1531,8 +923,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1531
923
|
...(channelToolDefs?.TOOL_DEFS || []).filter((tool) => channels.isChannelTool(tool?.name)),
|
|
1532
924
|
...(codeGraphToolDefs?.CODE_GRAPH_TOOL_DEFS || []).filter((tool) => tool?.name === 'code_graph'),
|
|
1533
925
|
...agentTool.tools,
|
|
1534
|
-
PROVIDER_STATUS_TOOL,
|
|
1535
|
-
CHANNEL_STATUS_TOOL,
|
|
1536
926
|
].map(applyStandaloneToolDefaults);
|
|
1537
927
|
bootProfile('tools:ready', { ms: (performance.now() - toolsStartedAt).toFixed(1), count: standaloneTools.length });
|
|
1538
928
|
|
|
@@ -1607,7 +997,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1607
997
|
if (name === 'explore') {
|
|
1608
998
|
const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
|
|
1609
999
|
return await runExplore(args || {}, {
|
|
1610
|
-
callerCwd: args?.cwd ? resolveCwdPath(
|
|
1000
|
+
callerCwd: args?.cwd ? resolveCwdPath(args.cwd) : callerCwd,
|
|
1611
1001
|
callerSessionId,
|
|
1612
1002
|
routingSessionId: callerSessionId,
|
|
1613
1003
|
clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
|
|
@@ -1617,7 +1007,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1617
1007
|
if (name === 'cwd') {
|
|
1618
1008
|
const action = clean(args?.action || (args?.path ? 'set' : 'get')).toLowerCase();
|
|
1619
1009
|
if (action === 'set') {
|
|
1620
|
-
applyResolvedCwd(resolveCwdPath(
|
|
1010
|
+
applyResolvedCwd(resolveCwdPath(args?.path));
|
|
1621
1011
|
} else if (action !== 'get') {
|
|
1622
1012
|
throw new Error(`cwd: unknown action "${action}"`);
|
|
1623
1013
|
}
|
|
@@ -1637,8 +1027,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1637
1027
|
notifyFn: notifyFnForSession(callerSessionId),
|
|
1638
1028
|
});
|
|
1639
1029
|
}
|
|
1640
|
-
if (name === 'provider_status') return renderProviderStatus(displayConfig());
|
|
1641
|
-
if (name === 'channel_status') return renderChannelStatus();
|
|
1642
1030
|
if (channels.isChannelTool(name)) {
|
|
1643
1031
|
if (!channelsEnabled()) throw new Error('channels are disabled in settings');
|
|
1644
1032
|
return await channels.execute(name, args || {});
|
|
@@ -1660,159 +1048,58 @@ export async function createMixdogSessionRuntime({
|
|
|
1660
1048
|
}
|
|
1661
1049
|
|
|
1662
1050
|
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;
|
|
1051
|
+
providerModelCaches.providerModelsCache = { models: null, at: 0 };
|
|
1052
|
+
providerModelCaches.providerModelsPromise = null;
|
|
1053
|
+
providerModelCaches.providerModelsLoadSeq += 1;
|
|
1054
|
+
providerModelCaches.searchProviderModelsCache = { models: null, at: 0 };
|
|
1055
|
+
providerUsageCaches.usageDashboardCache = { dashboard: null, at: 0 };
|
|
1056
|
+
providerUsageCaches.usageDashboardPromise = null;
|
|
1057
|
+
providerUsageCaches.providerSetupCache = { setup: null, at: 0 };
|
|
1058
|
+
providerUsageCaches.providerSetupQuickCache = { setup: null, at: 0 };
|
|
1059
|
+
providerUsageCaches.providerSetupPromise = null;
|
|
1672
1060
|
providerInitPromise = null;
|
|
1673
1061
|
modelMetaByRoute.clear();
|
|
1674
1062
|
}
|
|
1675
1063
|
|
|
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
|
-
}
|
|
1064
|
+
// Config reload/save/adopt family + output-style status cache. Extracted to
|
|
1065
|
+
// session-runtime/config-lifecycle.mjs; the facade retains ownership of the
|
|
1066
|
+
// config/searchRoute/configHasSecrets mutable locals via getter/setter
|
|
1067
|
+
// injection (the proven mutable-state pattern).
|
|
1068
|
+
const {
|
|
1069
|
+
getOutputStyleStatusCached,
|
|
1070
|
+
invalidateOutputStyleStatusCache,
|
|
1071
|
+
seedOutputStyleStatusCache,
|
|
1072
|
+
adoptConfig,
|
|
1073
|
+
saveConfigAndAdopt,
|
|
1074
|
+
flushConfigSave,
|
|
1075
|
+
flushBackendSave,
|
|
1076
|
+
scheduleBackendSave,
|
|
1077
|
+
flushOutputStyleSave,
|
|
1078
|
+
scheduleOutputStyleSave,
|
|
1079
|
+
reloadFullConfig,
|
|
1080
|
+
ensureFullConfig,
|
|
1081
|
+
displayConfig,
|
|
1082
|
+
ensureConfigForRouteProvider,
|
|
1083
|
+
} = createConfigLifecycle({
|
|
1084
|
+
getConfig: () => config,
|
|
1085
|
+
setConfig: (next) => { config = next; },
|
|
1086
|
+
getSearchRoute: () => searchRoute,
|
|
1087
|
+
setSearchRoute: (next) => { searchRoute = next; },
|
|
1088
|
+
getConfigHasSecrets: () => configHasSecrets,
|
|
1089
|
+
setConfigHasSecrets: (next) => { configHasSecrets = next; },
|
|
1090
|
+
getRoute: () => route,
|
|
1091
|
+
cfgMod,
|
|
1092
|
+
sharedCfgMod,
|
|
1093
|
+
setBackend,
|
|
1094
|
+
setConfiguredShell,
|
|
1095
|
+
normalizeSystemShellConfig,
|
|
1096
|
+
normalizeSearchRouteConfig,
|
|
1097
|
+
outputStyleStatus,
|
|
1098
|
+
LAZY_SECRET_PROVIDERS,
|
|
1099
|
+
clean,
|
|
1100
|
+
resolve,
|
|
1101
|
+
STANDALONE_DATA_DIR,
|
|
1102
|
+
});
|
|
1816
1103
|
|
|
1817
1104
|
async function ensureProvidersReady(providerConfig = config.providers || {}) {
|
|
1818
1105
|
if (providerInitPromise) return await providerInitPromise;
|
|
@@ -1823,499 +1110,113 @@ export async function createMixdogSessionRuntime({
|
|
|
1823
1110
|
return await providerInitPromise;
|
|
1824
1111
|
}
|
|
1825
1112
|
|
|
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
|
-
}
|
|
1113
|
+
const {
|
|
1114
|
+
currentMainSearchModelMeta,
|
|
1115
|
+
runNativeWebSearch,
|
|
1116
|
+
} = createNativeSearch({
|
|
1117
|
+
getRoute: () => route,
|
|
1118
|
+
getSearchRoute: () => searchRoute,
|
|
1119
|
+
setSearchRoute: (next) => { searchRoute = next; },
|
|
1120
|
+
getConfig: () => config,
|
|
1121
|
+
getSession: () => session,
|
|
1122
|
+
getReg: () => reg,
|
|
1123
|
+
ensureFullConfig,
|
|
1124
|
+
ensureProvidersReady,
|
|
1125
|
+
ensureProviderEnabled,
|
|
1126
|
+
normalizeSearchProviderId,
|
|
1127
|
+
normalizeSearchRouteConfig,
|
|
1128
|
+
isDefaultSearchRouteConfig,
|
|
1129
|
+
isSearchCapableProvider,
|
|
1130
|
+
searchCapableFor,
|
|
1131
|
+
});
|
|
2231
1132
|
|
|
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
|
-
}
|
|
1133
|
+
// Late-bound: createWarmupSchedulers is constructed after this factory, but
|
|
1134
|
+
// cachedProviderSetup(quick) may nudge scheduleProviderSetupWarmup on a cold
|
|
1135
|
+
// quick-cache fill. Thread it by reference so the scheduler is reachable once
|
|
1136
|
+
// it exists (a pre-scheduler quick fill simply skips the warmup nudge).
|
|
1137
|
+
let scheduleProviderSetupWarmupRef = () => {};
|
|
1138
|
+
const {
|
|
1139
|
+
refreshStatuslineUsageSnapshot,
|
|
1140
|
+
cachedProviderSetup,
|
|
1141
|
+
getUsageDashboard,
|
|
1142
|
+
} = createProviderUsage({
|
|
1143
|
+
caches: providerUsageCaches,
|
|
1144
|
+
getConfig: () => config,
|
|
1145
|
+
getReg: () => reg,
|
|
1146
|
+
displayConfig,
|
|
1147
|
+
providerSetup,
|
|
1148
|
+
createUsageDashboard,
|
|
1149
|
+
fetchOAuthUsageSnapshot,
|
|
1150
|
+
isCloseRequested: () => closeRequested,
|
|
1151
|
+
getProviderSetupWarmupTimer: () => warmupTimers.providerSetupWarmupTimer,
|
|
1152
|
+
scheduleProviderSetupWarmup: (delayMs) => scheduleProviderSetupWarmupRef(delayMs),
|
|
1153
|
+
});
|
|
2271
1154
|
|
|
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
|
-
|
|
1155
|
+
// Holder filled after createQuickModelRows resolves; provider-models and
|
|
1156
|
+
// quick-model-rows are mutually dependent (rows need cache-row helpers, the
|
|
1157
|
+
// model factory needs quick fallbacks) so we thread the quick surface in by
|
|
1158
|
+
// reference after both are constructed.
|
|
1159
|
+
const providerModelQuickHelpers = {};
|
|
1160
|
+
// Late-bound: createWarmupSchedulers is constructed after this factory, but
|
|
1161
|
+
// lookupModelMeta may fire scheduleProviderModelWarmup on a cache miss. Thread
|
|
1162
|
+
// it by reference so the scheduler is called once it exists (miss handling is
|
|
1163
|
+
// best-effort; a pre-scheduler miss simply skips the warmup nudge).
|
|
1164
|
+
let scheduleProviderModelWarmupRef = () => {};
|
|
1165
|
+
const {
|
|
1166
|
+
modelMetaKey,
|
|
1167
|
+
lookupModelMeta,
|
|
1168
|
+
sortProviderModels,
|
|
1169
|
+
providerModelCacheRow,
|
|
1170
|
+
providerModelsFromCacheRows,
|
|
1171
|
+
collectSearchProviderModels,
|
|
1172
|
+
collectProviderModels,
|
|
1173
|
+
warmProviderModelCache,
|
|
1174
|
+
} = createProviderModels({
|
|
1175
|
+
caches: providerModelCaches,
|
|
1176
|
+
modelMetaByRoute,
|
|
1177
|
+
getRoute: () => route,
|
|
1178
|
+
getConfig: () => config,
|
|
1179
|
+
getReg: () => reg,
|
|
1180
|
+
searchCapableFor,
|
|
1181
|
+
sortProviderModelsRaw,
|
|
1182
|
+
providerModelCacheRowRaw,
|
|
1183
|
+
normalizeSearchProviderId,
|
|
1184
|
+
isSearchCapableProvider,
|
|
1185
|
+
ensureFullConfig,
|
|
1186
|
+
ensureProvidersReady,
|
|
1187
|
+
bootProfile,
|
|
1188
|
+
scheduleProviderModelWarmup: () => scheduleProviderModelWarmupRef(),
|
|
1189
|
+
quickHelpers: providerModelQuickHelpers,
|
|
1190
|
+
});
|
|
2299
1191
|
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
1192
|
+
const {
|
|
1193
|
+
quickProviderModelRows,
|
|
1194
|
+
addDefaultSearchModel,
|
|
1195
|
+
quickSearchProviderModelRows,
|
|
1196
|
+
searchModelsFromRows,
|
|
1197
|
+
searchRowsWithDefault,
|
|
1198
|
+
} = createQuickModelRows({
|
|
1199
|
+
getRoute: () => route,
|
|
1200
|
+
getSearchRoute: () => searchRoute,
|
|
1201
|
+
displayConfig,
|
|
1202
|
+
providerModelCacheRow,
|
|
1203
|
+
providerModelsFromCacheRows,
|
|
1204
|
+
sortProviderModels,
|
|
1205
|
+
modelMetaByRoute,
|
|
1206
|
+
modelMetaKey,
|
|
1207
|
+
normalizeSearchProviderId,
|
|
1208
|
+
normalizeSearchRouteConfig,
|
|
1209
|
+
isSearchCapableProvider,
|
|
1210
|
+
searchCapableFor,
|
|
1211
|
+
currentMainSearchModelMeta,
|
|
1212
|
+
});
|
|
1213
|
+
Object.assign(providerModelQuickHelpers, {
|
|
1214
|
+
quickProviderModelRows,
|
|
1215
|
+
addDefaultSearchModel,
|
|
1216
|
+
quickSearchProviderModelRows,
|
|
1217
|
+
searchModelsFromRows,
|
|
1218
|
+
searchRowsWithDefault,
|
|
1219
|
+
});
|
|
2319
1220
|
|
|
2320
1221
|
async function resolveMissingRouteModelForFirstTurn() {
|
|
2321
1222
|
if (routeHasModel()) return route;
|
|
@@ -2432,85 +1333,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2432
1333
|
}
|
|
2433
1334
|
session = mgr.createSession(sessionOpts);
|
|
2434
1335
|
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
|
-
});
|
|
1336
|
+
attachSessionHooks(session, { hooks, hookCommonPayload, getCwd: () => currentCwd });
|
|
2514
1337
|
applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
2515
1338
|
applyPreSessionToolSelection();
|
|
2516
1339
|
writeStatuslineRoute(statusRoutes, session, route);
|
|
@@ -2549,212 +1372,133 @@ function parsedProviderModelVersion(id) {
|
|
|
2549
1372
|
}
|
|
2550
1373
|
}
|
|
2551
1374
|
|
|
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
|
-
}
|
|
1375
|
+
const {
|
|
1376
|
+
scheduleProviderWarmup,
|
|
1377
|
+
scheduleProviderSetupWarmup,
|
|
1378
|
+
scheduleProviderModelWarmup,
|
|
1379
|
+
scheduleModelCatalogWarmup,
|
|
1380
|
+
scheduleStatuslineUsageWarmup,
|
|
1381
|
+
scheduleStatuslineUsageRefresh,
|
|
1382
|
+
} = createWarmupSchedulers({
|
|
1383
|
+
timers: warmupTimers,
|
|
1384
|
+
bootProfile,
|
|
1385
|
+
getRoute: () => route,
|
|
1386
|
+
getConfig: () => config,
|
|
1387
|
+
isCloseRequested: () => closeRequested,
|
|
1388
|
+
getActiveTurnCount: () => activeTurnCount,
|
|
1389
|
+
getSessionCreatePromise: () => sessionCreatePromise,
|
|
1390
|
+
getProviderModelsCache: () => providerModelCaches.providerModelsCache,
|
|
1391
|
+
getProviderModelsPromise: () => providerModelCaches.providerModelsPromise,
|
|
1392
|
+
reloadFullConfig,
|
|
1393
|
+
ensureConfigForRouteProvider,
|
|
1394
|
+
ensureProvidersReady,
|
|
1395
|
+
ensureProviderEnabled,
|
|
1396
|
+
refreshStatuslineUsageSnapshot,
|
|
1397
|
+
warmProviderModelCache,
|
|
1398
|
+
cachedProviderSetup,
|
|
1399
|
+
warmCatalogsInBackground,
|
|
1400
|
+
isFirstTurnCompleted: () => firstTurnCompleted,
|
|
1401
|
+
envFlag,
|
|
1402
|
+
delays: {
|
|
1403
|
+
providerWarmupDelayMs,
|
|
1404
|
+
providerSetupWarmupDelayMs,
|
|
1405
|
+
providerModelWarmupDelayMs,
|
|
1406
|
+
modelCatalogWarmupDelayMs,
|
|
1407
|
+
statuslineUsageWarmupDelayMs,
|
|
1408
|
+
statuslineUsageRefreshDelayMs,
|
|
1409
|
+
backgroundBusyRetryMs,
|
|
1410
|
+
},
|
|
1411
|
+
flags: {
|
|
1412
|
+
providerWarmupEnabled,
|
|
1413
|
+
modelPrefetchEnabled,
|
|
1414
|
+
modelCatalogWarmupEnabled,
|
|
1415
|
+
},
|
|
1416
|
+
});
|
|
1417
|
+
scheduleProviderModelWarmupRef = scheduleProviderModelWarmup;
|
|
1418
|
+
scheduleProviderSetupWarmupRef = scheduleProviderSetupWarmup;
|
|
1419
|
+
|
|
1420
|
+
const {
|
|
1421
|
+
scheduleCodeGraphPrewarm,
|
|
1422
|
+
scheduleLeadSessionPrewarm,
|
|
1423
|
+
invokeChannelStart,
|
|
1424
|
+
scheduleChannelStart,
|
|
1425
|
+
} = createPrewarmSchedulers({
|
|
1426
|
+
timers: prewarmTimers,
|
|
1427
|
+
bootProfile,
|
|
1428
|
+
getCurrentCwd: () => currentCwd,
|
|
1429
|
+
isCloseRequested: () => closeRequested,
|
|
1430
|
+
getActiveTurnCount: () => activeTurnCount,
|
|
1431
|
+
getSessionCreatePromise: () => sessionCreatePromise,
|
|
1432
|
+
getSession: () => session,
|
|
1433
|
+
isRemoteEnabled: () => remoteEnabled,
|
|
1434
|
+
channelsEnabled,
|
|
1435
|
+
getCodeGraphModule,
|
|
1436
|
+
createCurrentSession,
|
|
1437
|
+
channels,
|
|
1438
|
+
envFlag,
|
|
1439
|
+
delays: {
|
|
1440
|
+
codeGraphPrewarmDelayMs,
|
|
1441
|
+
channelStartDelayMs,
|
|
1442
|
+
sessionPrewarmDelayMs,
|
|
1443
|
+
backgroundBusyRetryMs,
|
|
1444
|
+
},
|
|
1445
|
+
flags: {
|
|
1446
|
+
codeGraphPrewarmEnabled,
|
|
1447
|
+
sessionPrewarmEnabled,
|
|
1448
|
+
},
|
|
1449
|
+
state: prewarmState,
|
|
1450
|
+
});
|
|
2649
1451
|
|
|
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
|
-
}
|
|
1452
|
+
// Eagerly create/refresh the remote transcript writer for the CURRENT
|
|
1453
|
+
// session + cwd, publish the session record, and ensure the transcript
|
|
1454
|
+
// file exists on disk. Called from startRemote() (so the channel worker's
|
|
1455
|
+
// activate-time discovery finds THIS session immediately instead of
|
|
1456
|
+
// waiting for the first turn) and from ask() at turn start. Returns true
|
|
1457
|
+
// when a writer is bound.
|
|
1458
|
+
function ensureRemoteTranscriptWriter() {
|
|
1459
|
+
if (!remoteEnabled || !session?.id) return false;
|
|
1460
|
+
const twKey = `${session.id}\u0000${currentCwd}`;
|
|
1461
|
+
if (_twKey !== twKey) {
|
|
2665
1462
|
try {
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
1463
|
+
_transcriptWriter = createTranscriptWriter({
|
|
1464
|
+
mixdogHome: mixdogHome(),
|
|
1465
|
+
sessionId: session.id,
|
|
1466
|
+
cwd: currentCwd,
|
|
1467
|
+
pid: process.pid,
|
|
1468
|
+
});
|
|
1469
|
+
_transcriptWriter.writeSessionRecord();
|
|
1470
|
+
_twKey = twKey;
|
|
2671
1471
|
} 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;
|
|
2695
|
-
}
|
|
2696
|
-
try {
|
|
2697
|
-
ensureConfigForRouteProvider();
|
|
2698
|
-
await ensureProvidersReady(ensureProviderEnabled(config, route.provider));
|
|
2699
|
-
if (closeRequested) return;
|
|
2700
|
-
refreshStatuslineUsageSnapshot(route);
|
|
2701
|
-
} catch {
|
|
2702
|
-
// Usage display must never affect the session runtime.
|
|
2703
|
-
} finally {
|
|
2704
|
-
scheduleStatuslineUsageRefresh();
|
|
1472
|
+
process.stderr.write(`mixdog: transcript-writer: init failed: ${error?.message || error}\n`);
|
|
1473
|
+
_transcriptWriter = null;
|
|
1474
|
+
_twKey = '';
|
|
1475
|
+
return false;
|
|
2705
1476
|
}
|
|
2706
|
-
}
|
|
2707
|
-
|
|
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;
|
|
1477
|
+
} else {
|
|
1478
|
+
// Same binding — refresh updatedAt so worker-side discovery keeps
|
|
1479
|
+
// ranking this session as the live parent-chain candidate.
|
|
1480
|
+
try { _transcriptWriter?.writeSessionRecord(); } catch {}
|
|
2734
1481
|
}
|
|
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?.();
|
|
1482
|
+
try { _transcriptWriter?.ensureTranscriptFile(); }
|
|
1483
|
+
catch (error) { process.stderr.write(`mixdog: transcript-writer: ensureTranscriptFile failed: ${error?.message || error}\n`); }
|
|
1484
|
+
return _transcriptWriter != null;
|
|
2751
1485
|
}
|
|
2752
1486
|
|
|
2753
1487
|
// Remote (Discord channel) mode is opt-in per session. Only a session that
|
|
2754
1488
|
// explicitly enables remote — via `mixdog --remote` or the runtime toggle —
|
|
2755
1489
|
// boots the channel worker and contends for channel ownership.
|
|
1490
|
+
// startRemote() is FORCE-TAKEOVER: it always (re)claims the bridge seat and
|
|
1491
|
+
// rebinds output forwarding to this session, even when the worker is
|
|
1492
|
+
// already running (e.g. `/remote` re-issued after another session took the
|
|
1493
|
+
// seat, or to re-pin forwarding onto the current transcript).
|
|
2756
1494
|
function startRemote() {
|
|
2757
1495
|
remoteEnabled = true;
|
|
1496
|
+
// Publish this session's record + transcript file BEFORE the worker's
|
|
1497
|
+
// activate-time discovery polls, so output forwarding binds to this
|
|
1498
|
+
// terminal session immediately instead of waiting for the first turn.
|
|
1499
|
+
// No-op when the session has not been created yet (lazy mode); that
|
|
1500
|
+
// case is covered by the turn-start rebind in ask().
|
|
1501
|
+
ensureRemoteTranscriptWriter();
|
|
2758
1502
|
// A backend switch may still be sitting in its debounce window; flush it
|
|
2759
1503
|
// so the channel worker boots against the backend the user just chose,
|
|
2760
1504
|
// not the previous on-disk value.
|
|
@@ -2768,19 +1512,43 @@ function parsedProviderModelVersion(id) {
|
|
|
2768
1512
|
return true;
|
|
2769
1513
|
}
|
|
2770
1514
|
if (closeRequested) return true;
|
|
2771
|
-
if (channelStartTimer) {
|
|
2772
|
-
clearTimeout(channelStartTimer);
|
|
2773
|
-
channelStartTimer = null;
|
|
1515
|
+
if (prewarmTimers.channelStartTimer) {
|
|
1516
|
+
clearTimeout(prewarmTimers.channelStartTimer);
|
|
1517
|
+
prewarmTimers.channelStartTimer = null;
|
|
2774
1518
|
}
|
|
2775
1519
|
bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
|
|
2776
|
-
void
|
|
1520
|
+
void (async () => {
|
|
1521
|
+
// Immediate-occupancy guarantee: make sure a session + transcript
|
|
1522
|
+
// exist BEFORE the worker boots — a freshly-forked worker claims and
|
|
1523
|
+
// runs transcript discovery inside its own start(), so publishing the
|
|
1524
|
+
// session record/file first lets that very first discovery pass bind
|
|
1525
|
+
// output forwarding to THIS terminal instead of a persisted/stale
|
|
1526
|
+
// neighbour. Lazy mode means the session may not exist yet at /remote
|
|
1527
|
+
// time; create it here (idempotent — reuses a live session, joins an
|
|
1528
|
+
// in-flight create). On create failure we still claim: that matches
|
|
1529
|
+
// the pre-eager behavior (bind resolves on the first turn's rebind).
|
|
1530
|
+
try { await createCurrentSession('remote-start'); }
|
|
1531
|
+
catch (error) { bootProfile('channels:remote-session-create-failed', { error: error?.message || String(error) }); }
|
|
1532
|
+
ensureRemoteTranscriptWriter();
|
|
1533
|
+
// Re-check after the awaits above: stopRemote()/superseded or runtime
|
|
1534
|
+
// close may have landed mid-chain — do not boot/claim for a session
|
|
1535
|
+
// that already turned remote off.
|
|
1536
|
+
if (!remoteEnabled || closeRequested) return;
|
|
1537
|
+
await invokeChannelStart();
|
|
1538
|
+
if (!remoteEnabled || closeRequested) return;
|
|
1539
|
+
// Unconditional claim + forwarder rebind. A freshly-forked worker
|
|
1540
|
+
// already claims in its own start(), so this is idempotent there; the
|
|
1541
|
+
// already-running case is where it matters (last-wins seat overwrite +
|
|
1542
|
+
// transcript rebind onto this session).
|
|
1543
|
+
await channels.execute('activate_channel_bridge', { active: true });
|
|
1544
|
+
})().catch((error) => bootProfile('channels:claim-failed', { error: error?.message || String(error) }));
|
|
2777
1545
|
return true;
|
|
2778
1546
|
}
|
|
2779
1547
|
|
|
2780
1548
|
function stopRemote(reason) {
|
|
2781
1549
|
remoteEnabled = false;
|
|
2782
1550
|
// Cancel any pending deferred start so it can't fire after remote is off.
|
|
2783
|
-
if (channelStartTimer) { clearTimeout(channelStartTimer); channelStartTimer = null; }
|
|
1551
|
+
if (prewarmTimers.channelStartTimer) { clearTimeout(prewarmTimers.channelStartTimer); prewarmTimers.channelStartTimer = null; }
|
|
2784
1552
|
channels.stop(reason || 'remote-disabled', { waitForExit: false }).catch(() => {});
|
|
2785
1553
|
return true;
|
|
2786
1554
|
}
|
|
@@ -2879,7 +1647,63 @@ function parsedProviderModelVersion(id) {
|
|
|
2879
1647
|
return true;
|
|
2880
1648
|
}
|
|
2881
1649
|
|
|
1650
|
+
// Pure settings-delegate methods (onboarding status/skip, autoClear, profile,
|
|
1651
|
+
// compaction, recap/memory, channels, systemShell, update settings, channel
|
|
1652
|
+
// token save/forget, setBackend). Extracted to session-runtime/settings-api.mjs
|
|
1653
|
+
// and SPREAD into the API object below so the external surface is unchanged.
|
|
1654
|
+
const settingsApi = createSettingsApi({
|
|
1655
|
+
getConfig: () => config,
|
|
1656
|
+
getRoute: () => route,
|
|
1657
|
+
getSession: () => session,
|
|
1658
|
+
getRemoteEnabled: () => remoteEnabled,
|
|
1659
|
+
saveConfigAndAdopt,
|
|
1660
|
+
scheduleBackendSave,
|
|
1661
|
+
cfgMod,
|
|
1662
|
+
hasOwn,
|
|
1663
|
+
normalizeAutoClearConfig,
|
|
1664
|
+
autoClearIdleMsForProvider,
|
|
1665
|
+
normalizeCompactionConfig,
|
|
1666
|
+
normalizeCompactTypeSetting,
|
|
1667
|
+
normalizeSystemShellConfig,
|
|
1668
|
+
normalizeSystemShellCommand,
|
|
1669
|
+
setConfiguredShell,
|
|
1670
|
+
setRecapEnabledInConfig,
|
|
1671
|
+
setModuleEnabledInConfig,
|
|
1672
|
+
summarizeWorkflowRoutes,
|
|
1673
|
+
parseDurationMs,
|
|
1674
|
+
formatDurationMs,
|
|
1675
|
+
localPackageVersion,
|
|
1676
|
+
memoryEnabled,
|
|
1677
|
+
recapEnabledFn,
|
|
1678
|
+
channelsEnabled,
|
|
1679
|
+
autoUpdateEnabled,
|
|
1680
|
+
getUpdateCheckState: () => updateCheckState,
|
|
1681
|
+
getUpdateProcessState: () => updateProcessState,
|
|
1682
|
+
invalidateContextStatusCache: (...a) => invalidateContextStatusCache(...a),
|
|
1683
|
+
invalidatePreSessionToolSurface: (...a) => invalidatePreSessionToolSurface(...a),
|
|
1684
|
+
scheduleChannelStart: (...a) => scheduleChannelStart(...a),
|
|
1685
|
+
channels,
|
|
1686
|
+
clearChannelStartTimer: () => {
|
|
1687
|
+
if (prewarmTimers.channelStartTimer) {
|
|
1688
|
+
clearTimeout(prewarmTimers.channelStartTimer);
|
|
1689
|
+
prewarmTimers.channelStartTimer = null;
|
|
1690
|
+
}
|
|
1691
|
+
},
|
|
1692
|
+
checkForUpdateInternal: (...a) => checkForUpdateInternal(...a),
|
|
1693
|
+
runUpdateNowInternal: (...a) => runUpdateNowInternal(...a),
|
|
1694
|
+
reloadChannelsSoon: (...a) => reloadChannelsSoon(...a),
|
|
1695
|
+
ONBOARDING_VERSION,
|
|
1696
|
+
saveDiscordToken,
|
|
1697
|
+
forgetDiscordToken,
|
|
1698
|
+
saveTelegramToken,
|
|
1699
|
+
forgetTelegramToken,
|
|
1700
|
+
saveWebhookAuthtoken,
|
|
1701
|
+
forgetWebhookAuthtoken,
|
|
1702
|
+
setBackend,
|
|
1703
|
+
});
|
|
1704
|
+
|
|
2882
1705
|
return {
|
|
1706
|
+
...settingsApi,
|
|
2883
1707
|
get id() {
|
|
2884
1708
|
return session?.id || null;
|
|
2885
1709
|
},
|
|
@@ -2914,7 +1738,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2914
1738
|
return mode;
|
|
2915
1739
|
},
|
|
2916
1740
|
get autoClear() {
|
|
2917
|
-
return
|
|
1741
|
+
return this.getAutoClear();
|
|
2918
1742
|
},
|
|
2919
1743
|
get systemShell() {
|
|
2920
1744
|
return normalizeSystemShellConfig(config.shell);
|
|
@@ -3046,244 +1870,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3046
1870
|
return await cachedProviderSetup();
|
|
3047
1871
|
},
|
|
3048
1872
|
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 };
|
|
1873
|
+
return await getUsageDashboard(options);
|
|
3287
1874
|
},
|
|
3288
1875
|
async completeOnboarding(payload = {}) {
|
|
3289
1876
|
// Only fall back to the live runtime route when the caller actually sent a
|
|
@@ -3408,52 +1995,13 @@ function parsedProviderModelVersion(id) {
|
|
|
3408
1995
|
isRemoteEnabled() {
|
|
3409
1996
|
return isRemoteEnabled();
|
|
3410
1997
|
},
|
|
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;
|
|
1998
|
+
// Subscribe to non-user-initiated remote flips (seat superseded). Returns
|
|
1999
|
+
// an unsubscribe function. TUI uses this to sync its Remote indicator and
|
|
2000
|
+
// show a "remote taken over" notice.
|
|
2001
|
+
onRemoteStateChange(listener) {
|
|
2002
|
+
if (typeof listener !== 'function') return () => {};
|
|
2003
|
+
remoteStateListeners.add(listener);
|
|
2004
|
+
return () => remoteStateListeners.delete(listener);
|
|
3457
2005
|
},
|
|
3458
2006
|
saveChannel(entry) {
|
|
3459
2007
|
const result = saveChannel(entry);
|
|
@@ -3685,18 +2233,13 @@ function parsedProviderModelVersion(id) {
|
|
|
3685
2233
|
nextConfig.agent = agent;
|
|
3686
2234
|
}
|
|
3687
2235
|
adoptConfig(nextConfig);
|
|
3688
|
-
|
|
3689
|
-
if (outputStyleSaveTimer) clearTimeout(outputStyleSaveTimer);
|
|
3690
|
-
outputStyleSaveTimer = setTimeout(flushOutputStyleSave, CONFIG_SAVE_DEBOUNCE_MS);
|
|
3691
|
-
outputStyleSaveTimer.unref?.();
|
|
2236
|
+
scheduleOutputStyleSave(selected.id);
|
|
3692
2237
|
// Reuse the catalog already scanned above for `before` instead of a
|
|
3693
2238
|
// second forced-fresh filesystem scan; refresh the status cache
|
|
3694
2239
|
// in-memory (short TTL, same as getOutputStyleStatusCached) so reads
|
|
3695
2240
|
// during the debounce window see the just-selected style.
|
|
3696
2241
|
const freshStatus = { configured: selected.id, current: selected, styles: before.styles };
|
|
3697
|
-
|
|
3698
|
-
outputStyleStatusCacheAt = performance.now();
|
|
3699
|
-
outputStyleStatusCacheDir = resolve(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR);
|
|
2242
|
+
seedOutputStyleStatusCache(freshStatus);
|
|
3700
2243
|
const hasConversation = sessionHasConversationMessages(session);
|
|
3701
2244
|
let appliedToCurrentSession = !hasConversation;
|
|
3702
2245
|
if (session?.id && !hasConversation) {
|
|
@@ -3791,25 +2334,28 @@ function parsedProviderModelVersion(id) {
|
|
|
3791
2334
|
// JSONL and pushes the surface view to Discord. Gated on remoteEnabled
|
|
3792
2335
|
// so non-remote sessions write nothing.
|
|
3793
2336
|
if (remoteEnabled) {
|
|
3794
|
-
const twKey = `${session.id}\u0000${currentCwd}`;
|
|
3795
2337
|
// Reset per-turn dedup tracker so an identical answer in a later turn
|
|
3796
2338
|
// is still written (the guard only prevents same-turn double-write).
|
|
3797
2339
|
_lastAppendedAssistant = '';
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
2340
|
+
const prevKey = _twKey;
|
|
2341
|
+
ensureRemoteTranscriptWriter();
|
|
2342
|
+
// The writer moved to a NEW session/cwd (e.g. lazy first-turn
|
|
2343
|
+
// session create after /remote, or /new mid-remote). Re-pin the
|
|
2344
|
+
// channel worker's output forwarder onto the fresh transcript NOW —
|
|
2345
|
+
// otherwise it keeps tailing the previous binding until an inbound
|
|
2346
|
+
// Discord message triggers the steal path, so terminal-initiated
|
|
2347
|
+
// turns would never forward. activate_channel_bridge is idempotent
|
|
2348
|
+
// force-takeover (claim + rebind); fire-and-forget so the turn is
|
|
2349
|
+
// never blocked on worker IPC.
|
|
2350
|
+
if (_twKey && _twKey !== prevKey && channelsEnabled() && !envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
2351
|
+
void invokeChannelStart()
|
|
2352
|
+
.then(() => {
|
|
2353
|
+
// stopRemote()/superseded/close may have landed while the
|
|
2354
|
+
// worker was starting — never re-claim for a turned-off session.
|
|
2355
|
+
if (!remoteEnabled || closeRequested) return undefined;
|
|
2356
|
+
return channels.execute('activate_channel_bridge', { active: true });
|
|
2357
|
+
})
|
|
2358
|
+
.catch((error) => bootProfile('channels:turn-rebind-failed', { error: error?.message || String(error) }));
|
|
3813
2359
|
}
|
|
3814
2360
|
}
|
|
3815
2361
|
hooks.emit('turn:start', { sessionId: session.id, prompt, cwd: currentCwd });
|
|
@@ -4037,7 +2583,7 @@ function parsedProviderModelVersion(id) {
|
|
|
4037
2583
|
return { ...result, status: this.toolsStatus() };
|
|
4038
2584
|
},
|
|
4039
2585
|
setCwd(path) {
|
|
4040
|
-
applyResolvedCwd(resolveCwdPath(
|
|
2586
|
+
applyResolvedCwd(resolveCwdPath(path));
|
|
4041
2587
|
return currentCwd;
|
|
4042
2588
|
},
|
|
4043
2589
|
mcpStatus() {
|
|
@@ -4248,9 +2794,19 @@ function parsedProviderModelVersion(id) {
|
|
|
4248
2794
|
...(args || {}),
|
|
4249
2795
|
query: baseQuery,
|
|
4250
2796
|
cwd: args?.cwd || currentCwd,
|
|
2797
|
+
// Grouping hint for multi-session browse output: lets the memory
|
|
2798
|
+
// service mark THIS session's group as "(current)". Not a filter.
|
|
2799
|
+
...(session?.id ? { currentSessionId: session.id } : {}),
|
|
4251
2800
|
};
|
|
4252
2801
|
let result = '(no results)';
|
|
4253
2802
|
if (session?.id && args?.currentSession !== false && args?.forceCycleOnEmpty !== false) {
|
|
2803
|
+
// Empty-fallback: hydrate the current transcript into the memory DB
|
|
2804
|
+
// (no LLM — ingest only) and re-search session-scoped with the raw
|
|
2805
|
+
// leg on. The old path also ran a synchronous cycle1 (LLM chunking)
|
|
2806
|
+
// here, which made an empty recall the slowest possible recall and
|
|
2807
|
+
// called the LLM even with recap/memory off; the raw rows are
|
|
2808
|
+
// searchable directly (FTS/trgm + post-ingest raw embeddings), so
|
|
2809
|
+
// chunking is left to the background cycle.
|
|
4254
2810
|
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
4255
2811
|
if (messages.length > 0) {
|
|
4256
2812
|
await memoryMod.handleToolCall('memory', {
|
|
@@ -4259,12 +2815,6 @@ function parsedProviderModelVersion(id) {
|
|
|
4259
2815
|
cwd: currentCwd,
|
|
4260
2816
|
messages,
|
|
4261
2817
|
});
|
|
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
2818
|
result = toolResponseText(await memoryMod.handleToolCall('recall', {
|
|
4269
2819
|
...baseArgs,
|
|
4270
2820
|
sessionId: session.id,
|
|
@@ -4417,37 +2967,30 @@ function parsedProviderModelVersion(id) {
|
|
|
4417
2967
|
try { flushConfigSave(); } catch {}
|
|
4418
2968
|
try { flushBackendSave(); } catch {}
|
|
4419
2969
|
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;
|
|
2970
|
+
if (prewarmTimers.channelStartTimer) {
|
|
2971
|
+
clearTimeout(prewarmTimers.channelStartTimer);
|
|
2972
|
+
prewarmTimers.channelStartTimer = null;
|
|
2973
|
+
}
|
|
2974
|
+
for (const timerKey of [
|
|
2975
|
+
'providerSetupWarmupTimer',
|
|
2976
|
+
'providerWarmupTimer',
|
|
2977
|
+
'providerModelWarmupTimer',
|
|
2978
|
+
'modelCatalogWarmupTimer',
|
|
2979
|
+
]) {
|
|
2980
|
+
if (warmupTimers[timerKey]) {
|
|
2981
|
+
clearTimeout(warmupTimers[timerKey]);
|
|
2982
|
+
warmupTimers[timerKey] = null;
|
|
2983
|
+
}
|
|
4443
2984
|
}
|
|
4444
|
-
if (
|
|
4445
|
-
clearTimeout(
|
|
4446
|
-
|
|
2985
|
+
if (prewarmTimers.codeGraphPrewarmTimer) {
|
|
2986
|
+
clearTimeout(prewarmTimers.codeGraphPrewarmTimer);
|
|
2987
|
+
prewarmTimers.codeGraphPrewarmTimer = null;
|
|
4447
2988
|
}
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
2989
|
+
for (const timerKey of ['statuslineUsageWarmupTimer', 'statuslineUsageRefreshTimer']) {
|
|
2990
|
+
if (warmupTimers[timerKey]) {
|
|
2991
|
+
clearTimeout(warmupTimers[timerKey]);
|
|
2992
|
+
warmupTimers[timerKey] = null;
|
|
2993
|
+
}
|
|
4451
2994
|
}
|
|
4452
2995
|
try { cancelBackgroundTasks({ reason, notify: false }); } catch {}
|
|
4453
2996
|
const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
|
|
@@ -4471,7 +3014,19 @@ function parsedProviderModelVersion(id) {
|
|
|
4471
3014
|
let ok = false;
|
|
4472
3015
|
if (session?.id) {
|
|
4473
3016
|
statusRoutes?.clearGatewaySessionRoute?.(session.id);
|
|
4474
|
-
|
|
3017
|
+
// Bug fix: runtime stop/exit (TUI Ctrl-C, process exit) previously
|
|
3018
|
+
// always tombstoned the current session, so a session you were
|
|
3019
|
+
// mid-conversation in vanished from the Resume list the instant you
|
|
3020
|
+
// quit and was hard-deleted by the 24h tombstone sweep. Only
|
|
3021
|
+
// tombstone truly-empty scratch sessions; non-empty sessions must
|
|
3022
|
+
// survive exit resumable.
|
|
3023
|
+
// liveTurnMessages holds the in-flight user prompt until turn
|
|
3024
|
+
// commit — an active first-turn ask has its user message there,
|
|
3025
|
+
// not yet in session.messages, so it must also be checked or a
|
|
3026
|
+
// first-turn exit could still burn a real session.
|
|
3027
|
+
const tombstone = !hasUserConversationMessage(session.messages)
|
|
3028
|
+
&& !hasUserConversationMessage(session.liveTurnMessages);
|
|
3029
|
+
ok = mgr.closeSession(session.id, reason, { tombstone });
|
|
4475
3030
|
session = null;
|
|
4476
3031
|
}
|
|
4477
3032
|
const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
|
|
@@ -4518,7 +3073,10 @@ function parsedProviderModelVersion(id) {
|
|
|
4518
3073
|
const agent = clean(s.agent || '').toLowerCase();
|
|
4519
3074
|
const leadish = agent === 'lead'
|
|
4520
3075
|
|| sourceType === 'lead'
|
|
4521
|
-
|
|
3076
|
+
// Bug fix: side-terminal cli sessions have a non-empty/non-'main' sourceName
|
|
3077
|
+
// (e.g. terminal id) and were being hidden from resume even though they are
|
|
3078
|
+
// legitimate user sessions, not agent-owned. Any sourceName is fine for cli.
|
|
3079
|
+
|| (sourceType === 'cli')
|
|
4522
3080
|
|| (!sourceType && !sourceName && !isAgentOwner(owner));
|
|
4523
3081
|
if (!leadish) return null;
|
|
4524
3082
|
let preview = cleanSessionPreview(s.preview || '');
|
|
@@ -4532,7 +3090,12 @@ function parsedProviderModelVersion(id) {
|
|
|
4532
3090
|
preview = userPreviews[userPreviews.length - 1] || userPreviews[0] || '';
|
|
4533
3091
|
messageCount = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant')).length;
|
|
4534
3092
|
}
|
|
4535
|
-
|
|
3093
|
+
// Bug fix: sessions whose preview couldn't be derived (e.g. noise-only user
|
|
3094
|
+
// turns) were silently dropped from the resume list even when they had real
|
|
3095
|
+
// messages. Keep the row and let preview fall back to '' (TUI renders
|
|
3096
|
+
// '(no message)' for empty preview); only drop truly-empty scratch sessions
|
|
3097
|
+
// with zero visible messages.
|
|
3098
|
+
if (!preview && messageCount === 0) return null;
|
|
4536
3099
|
return {
|
|
4537
3100
|
id: s.id,
|
|
4538
3101
|
updatedAt: s.updatedAt,
|
|
@@ -4545,17 +3108,44 @@ function parsedProviderModelVersion(id) {
|
|
|
4545
3108
|
}).filter(Boolean);
|
|
4546
3109
|
},
|
|
4547
3110
|
async newSession() {
|
|
4548
|
-
if (session?.id)
|
|
3111
|
+
if (session?.id) {
|
|
3112
|
+
// Bug fix: /new used to unconditionally tombstone the outgoing
|
|
3113
|
+
// session, so switching to a fresh session burned whatever you'd
|
|
3114
|
+
// been working on — it dropped off the Resume list immediately and
|
|
3115
|
+
// was hard-deleted after the 24h tombstone sweep. Only tombstone
|
|
3116
|
+
// truly-empty scratch sessions; keep non-empty ones resumable.
|
|
3117
|
+
const tombstone = !hasUserConversationMessage(session.messages)
|
|
3118
|
+
&& !hasUserConversationMessage(session.liveTurnMessages);
|
|
3119
|
+
mgr.closeSession(session.id, 'cli-new', { tombstone });
|
|
3120
|
+
// Bug fix: closeSession({tombstone:false}) keeps the outgoing
|
|
3121
|
+
// session file intact so it stays resumable — but that meant
|
|
3122
|
+
// createCurrentSession()'s live-session reuse check (session?.id →
|
|
3123
|
+
// mgr.getSession(...) not closed) happily reloaded the SAME session,
|
|
3124
|
+
// so /new never actually reset the transcript and the context gauge
|
|
3125
|
+
// never returned to 0. Drop the in-memory reference so a fresh
|
|
3126
|
+
// session is always created.
|
|
3127
|
+
session = null;
|
|
3128
|
+
}
|
|
3129
|
+
invalidateContextStatusCache();
|
|
4549
3130
|
await createCurrentSession();
|
|
4550
3131
|
return session.id;
|
|
4551
3132
|
},
|
|
4552
3133
|
async resume(id) {
|
|
4553
3134
|
const previousId = session?.id || null;
|
|
3135
|
+
const previousMessages = session?.messages || null;
|
|
3136
|
+
const previousLive = session?.liveTurnMessages || null;
|
|
4554
3137
|
const resumed = await mgr.resumeSession(id, toolSpecForMode(mode));
|
|
4555
3138
|
if (!resumed) return null;
|
|
4556
3139
|
if (previousId && previousId !== resumed.id) {
|
|
4557
3140
|
statusRoutes?.clearGatewaySessionRoute?.(previousId);
|
|
4558
|
-
|
|
3141
|
+
// Bug fix: /resume used to unconditionally tombstone the session
|
|
3142
|
+
// you were switching away from, so it vanished from the Resume
|
|
3143
|
+
// list right away and was hard-deleted after the 24h sweep — the
|
|
3144
|
+
// exact "burn a resumable session" bug this fix targets. Only
|
|
3145
|
+
// tombstone truly-empty scratch sessions.
|
|
3146
|
+
const tombstone = !hasUserConversationMessage(previousMessages)
|
|
3147
|
+
&& !hasUserConversationMessage(previousLive);
|
|
3148
|
+
mgr.closeSession(previousId, 'cli-resume', { tombstone });
|
|
4559
3149
|
}
|
|
4560
3150
|
session = resumed;
|
|
4561
3151
|
currentCwd = resumed.cwd || currentCwd;
|