mixdog 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -23
- package/package.json +34 -28
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +205 -33
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1222 -90
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +108 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +519 -118
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +104 -473
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +44 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +139 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -95
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +428 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +756 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/ink.js +54 -8
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -3,14 +3,23 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { performance } from 'node:perf_hooks';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { ensureStandaloneEnvironment } from './standalone/seeds.mjs';
|
|
7
|
+
import { createStandaloneAgent } from './standalone/agent-tool.mjs';
|
|
8
|
+
import { isAgentOwner } from './runtime/agent/orchestrator/agent-owner.mjs';
|
|
8
9
|
import { EXPLORE_TOOL, runExplore } from './standalone/explore-tool.mjs';
|
|
9
10
|
import { createStandaloneChannelWorker } from './standalone/channel-worker.mjs';
|
|
11
|
+
import { createStandaloneMemoryRuntime } from './standalone/memory-runtime-proxy.mjs';
|
|
10
12
|
import { createStandaloneHookBus } from './standalone/hook-bus.mjs';
|
|
11
13
|
import { writeLastSessionCwd } from './runtime/shared/user-cwd.mjs';
|
|
12
14
|
import { cancelBackgroundTasks } from './runtime/shared/background-tasks.mjs';
|
|
13
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
modelVisibleToolCompletionMessage,
|
|
17
|
+
shouldPersistModelVisibleToolCompletion,
|
|
18
|
+
} from './runtime/shared/tool-execution-contract.mjs';
|
|
19
|
+
import {
|
|
20
|
+
normalizeAgentPermissionOrNone,
|
|
21
|
+
readMarkdownDocument,
|
|
22
|
+
} from './runtime/shared/markdown-frontmatter.mjs';
|
|
14
23
|
import { setConfiguredShell } from './runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs';
|
|
15
24
|
import {
|
|
16
25
|
PROVIDER_STATUS_TOOL,
|
|
@@ -25,6 +34,12 @@ import {
|
|
|
25
34
|
setLocalProvider,
|
|
26
35
|
} from './standalone/provider-admin.mjs';
|
|
27
36
|
import { createUsageDashboard } from './standalone/usage-dashboard.mjs';
|
|
37
|
+
import { fetchOAuthUsageSnapshot } from './runtime/agent/orchestrator/providers/oauth-usage.mjs';
|
|
38
|
+
import { getModelMetadataSync } from './runtime/agent/orchestrator/providers/model-catalog.mjs';
|
|
39
|
+
import {
|
|
40
|
+
isResponsesFreeformTool,
|
|
41
|
+
toResponsesCustomTool,
|
|
42
|
+
} from './runtime/agent/orchestrator/providers/custom-tool-wire.mjs';
|
|
28
43
|
import {
|
|
29
44
|
channelSetup,
|
|
30
45
|
deleteChannel,
|
|
@@ -52,9 +67,76 @@ import {
|
|
|
52
67
|
import {
|
|
53
68
|
estimateMessagesTokens,
|
|
54
69
|
estimateRequestReserveTokens,
|
|
70
|
+
estimateTranscriptContextUsage,
|
|
55
71
|
estimateToolSchemaTokens,
|
|
56
72
|
} from './runtime/agent/orchestrator/session/context-utils.mjs';
|
|
57
73
|
|
|
74
|
+
// Default compaction buffer rules — kept in lockstep with the worker runtime
|
|
75
|
+
// (loop.mjs resolveCompactBufferRatio / manager.mjs compactBufferRatioForSession
|
|
76
|
+
// and compact.mjs compactionBufferTokensForBoundary). By default the trigger is
|
|
77
|
+
// the effective compact boundary itself; explicit buffer settings can lower it.
|
|
78
|
+
const CONTEXT_DEFAULT_BUFFER_RATIO = 0;
|
|
79
|
+
const CONTEXT_MAX_BUFFER_RATIO = 0.25;
|
|
80
|
+
const CONTEXT_LEGACY_DEFAULT_BUFFER_RATIO = 0.1;
|
|
81
|
+
function resolveContextBufferRatio(cfg = {}) {
|
|
82
|
+
// Percent-named inputs (bufferPercent/bufferPct/*_BUFFER_PERCENT): a value of
|
|
83
|
+
// 1 means 1% (→0.01). Ratio-named inputs (bufferRatio/bufferFraction): 0.01
|
|
84
|
+
// means 1%, while a value > 1 is treated as a legacy percent (10 → 10%).
|
|
85
|
+
const candidates = [
|
|
86
|
+
[cfg.bufferPercent, true],
|
|
87
|
+
[cfg.bufferPct, true],
|
|
88
|
+
[cfg.bufferRatio, false],
|
|
89
|
+
[cfg.bufferFraction, false],
|
|
90
|
+
[process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT, true],
|
|
91
|
+
[process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO, false],
|
|
92
|
+
];
|
|
93
|
+
for (const [raw, isPercent] of candidates) {
|
|
94
|
+
const n = Number(raw);
|
|
95
|
+
if (!Number.isFinite(n) || n <= 0) continue;
|
|
96
|
+
return isPercent ? Math.min(1, n / 100) : (n > 1 ? n / 100 : n);
|
|
97
|
+
}
|
|
98
|
+
return CONTEXT_DEFAULT_BUFFER_RATIO;
|
|
99
|
+
}
|
|
100
|
+
function isLegacyDefaultContextBufferTelemetry(cfg = {}, boundaryTokens = 0) {
|
|
101
|
+
const boundary = Number(boundaryTokens);
|
|
102
|
+
if (!Number.isFinite(boundary) || boundary <= 0) return false;
|
|
103
|
+
if (Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_TOKENS) > 0) return false;
|
|
104
|
+
for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
|
|
105
|
+
const n = Number(process.env[envName]);
|
|
106
|
+
if (Number.isFinite(n) && n > 0) return false;
|
|
107
|
+
}
|
|
108
|
+
for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
|
|
109
|
+
const n = Number(cfg?.[key]);
|
|
110
|
+
if (Number.isFinite(n) && n > 0) return false;
|
|
111
|
+
}
|
|
112
|
+
const explicitTokens = Number(cfg?.bufferTokens ?? cfg?.buffer);
|
|
113
|
+
const ratio = Number(cfg?.bufferRatio);
|
|
114
|
+
if (!Number.isFinite(explicitTokens) || explicitTokens <= 0 || !Number.isFinite(ratio) || Math.abs(ratio - CONTEXT_LEGACY_DEFAULT_BUFFER_RATIO) > 1e-9) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const expectedTokens = Math.floor(boundary * CONTEXT_LEGACY_DEFAULT_BUFFER_RATIO);
|
|
118
|
+
const cfgBoundary = Number(cfg?.boundaryTokens);
|
|
119
|
+
const cfgTrigger = Number(cfg?.triggerTokens);
|
|
120
|
+
return Math.floor(explicitTokens) === expectedTokens
|
|
121
|
+
|| (Number.isFinite(cfgBoundary) && Math.floor(cfgBoundary) === Math.floor(boundary)
|
|
122
|
+
&& Number.isFinite(cfgTrigger) && cfgTrigger > 0
|
|
123
|
+
&& Math.floor(explicitTokens) === Math.max(0, Math.floor(boundary - cfgTrigger)));
|
|
124
|
+
}
|
|
125
|
+
function contextDefaultBufferTokens(boundaryTokens, cfg = {}) {
|
|
126
|
+
const boundary = Number(boundaryTokens);
|
|
127
|
+
if (!Number.isFinite(boundary) || boundary <= 0) return 0;
|
|
128
|
+
const effectiveCfg = isLegacyDefaultContextBufferTelemetry(cfg, boundary)
|
|
129
|
+
? { ...cfg, bufferTokens: null, buffer: null, bufferRatio: null }
|
|
130
|
+
: cfg;
|
|
131
|
+
const cap = Math.max(0, Math.floor(boundary * CONTEXT_MAX_BUFFER_RATIO));
|
|
132
|
+
const explicit = Number(effectiveCfg.bufferTokens ?? effectiveCfg.buffer);
|
|
133
|
+
if (Number.isFinite(explicit) && explicit > 0) {
|
|
134
|
+
return Math.min(Math.floor(explicit), cap);
|
|
135
|
+
}
|
|
136
|
+
const ratio = resolveContextBufferRatio(effectiveCfg);
|
|
137
|
+
return Math.max(0, Math.min(Math.floor(boundary * ratio), cap));
|
|
138
|
+
}
|
|
139
|
+
|
|
58
140
|
function sessionMessageText(content) {
|
|
59
141
|
if (content == null) return '';
|
|
60
142
|
if (typeof content === 'string') return content;
|
|
@@ -113,9 +195,8 @@ function splitMarkdownSections(text) {
|
|
|
113
195
|
function reminderSectionBucket(section) {
|
|
114
196
|
const heading = String(section.match(/^#\s+([^\n]+)/)?.[1] || '').trim().toLowerCase();
|
|
115
197
|
if (heading.includes('core memory')) return 'memory';
|
|
116
|
-
if (heading.includes('mixdog-project-context') || heading.includes('project-context')) return 'project';
|
|
117
198
|
if (heading.includes('active workflow') || heading.includes('available agents') || heading.includes('workflow')) return 'workflow';
|
|
118
|
-
if (heading.includes('workspace')
|
|
199
|
+
if (heading.includes('workspace')) return 'workspace';
|
|
119
200
|
if (heading.includes('environment')) return 'environment';
|
|
120
201
|
return 'other';
|
|
121
202
|
}
|
|
@@ -134,7 +215,6 @@ function summarizeContextMessages(messages) {
|
|
|
134
215
|
assistant: { count: 0, tokens: 0 },
|
|
135
216
|
toolResults: { count: 0, tokens: 0 },
|
|
136
217
|
reminders: { count: 0, tokens: 0, otherTokens: 0 },
|
|
137
|
-
project: { tokens: 0 },
|
|
138
218
|
workflow: { tokens: 0 },
|
|
139
219
|
memory: { tokens: 0 },
|
|
140
220
|
workspace: { tokens: 0 },
|
|
@@ -252,6 +332,10 @@ function envFlag(name) {
|
|
|
252
332
|
return /^(1|true|yes|on)$/i.test(String(process.env[name] || ''));
|
|
253
333
|
}
|
|
254
334
|
|
|
335
|
+
function envPresent(name) {
|
|
336
|
+
return process.env[name] !== undefined && process.env[name] !== '';
|
|
337
|
+
}
|
|
338
|
+
|
|
255
339
|
function envDelayMs(name, fallback, { min = 0, max = 60_000 } = {}) {
|
|
256
340
|
const raw = process.env[name];
|
|
257
341
|
if (raw === undefined || raw === '') return fallback;
|
|
@@ -292,7 +376,7 @@ async function profiledImport(label, spec, { optional = false } = {}) {
|
|
|
292
376
|
const EFFORT_OPTIONS_BY_PROVIDER = {
|
|
293
377
|
openai: ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
294
378
|
'openai-oauth': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
295
|
-
anthropic: ['low', 'medium', 'high', 'max'],
|
|
379
|
+
anthropic: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
296
380
|
'anthropic-oauth': ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
297
381
|
xai: ['none', 'low', 'medium', 'high'],
|
|
298
382
|
'grok-oauth': ['none', 'low', 'medium', 'high'],
|
|
@@ -329,15 +413,15 @@ export const TOOL_SEARCH_TOOL = {
|
|
|
329
413
|
destructiveHint: false,
|
|
330
414
|
idempotentHint: true,
|
|
331
415
|
openWorldHint: false,
|
|
332
|
-
|
|
416
|
+
agentHidden: true,
|
|
333
417
|
},
|
|
334
|
-
description: 'Search
|
|
418
|
+
description: 'Search deferred tools; confident queries load matches.',
|
|
335
419
|
inputSchema: {
|
|
336
420
|
type: 'object',
|
|
337
421
|
properties: {
|
|
338
|
-
query: { type: 'string', description: '
|
|
339
|
-
select: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: '
|
|
340
|
-
limit: { type: 'number', description: '
|
|
422
|
+
query: { type: 'string', description: 'Search text; confident hits load. select:a,b forces exact names.' },
|
|
423
|
+
select: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Force exact tool names.' },
|
|
424
|
+
limit: { type: 'number', description: 'Max matches.' },
|
|
341
425
|
},
|
|
342
426
|
additionalProperties: false,
|
|
343
427
|
},
|
|
@@ -352,43 +436,66 @@ const CHANNEL_STATUS_TOOL = {
|
|
|
352
436
|
destructiveHint: false,
|
|
353
437
|
idempotentHint: true,
|
|
354
438
|
openWorldHint: false,
|
|
355
|
-
|
|
439
|
+
agentHidden: true,
|
|
356
440
|
},
|
|
357
|
-
description: 'List
|
|
441
|
+
description: 'List channel/schedule/webhook status. No secrets.',
|
|
358
442
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
359
443
|
};
|
|
360
444
|
|
|
361
445
|
const CWD_TOOL = {
|
|
362
446
|
name: 'cwd',
|
|
363
|
-
title: '
|
|
447
|
+
title: 'Work Project',
|
|
364
448
|
annotations: {
|
|
365
|
-
title: '
|
|
449
|
+
title: 'Work Project',
|
|
366
450
|
readOnlyHint: false,
|
|
367
451
|
destructiveHint: false,
|
|
368
452
|
idempotentHint: false,
|
|
369
453
|
openWorldHint: false,
|
|
370
|
-
|
|
454
|
+
agentHidden: true,
|
|
371
455
|
},
|
|
372
|
-
description: 'Show or set the
|
|
456
|
+
description: 'Show or set the session work project for tool execution.',
|
|
373
457
|
inputSchema: {
|
|
374
458
|
type: 'object',
|
|
375
459
|
properties: {
|
|
376
|
-
action: { type: 'string', enum: ['get', 'set'], description: 'Default get
|
|
377
|
-
path: { type: 'string', description: '
|
|
460
|
+
action: { type: 'string', enum: ['get', 'set'], description: 'Default get.' },
|
|
461
|
+
path: { type: 'string', description: 'Project directory for set.' },
|
|
378
462
|
},
|
|
379
463
|
additionalProperties: false,
|
|
380
464
|
},
|
|
381
465
|
};
|
|
382
466
|
|
|
467
|
+
export const SKILL_TOOL = {
|
|
468
|
+
name: 'Skill',
|
|
469
|
+
title: 'Skill',
|
|
470
|
+
annotations: {
|
|
471
|
+
title: 'Skill',
|
|
472
|
+
readOnlyHint: true,
|
|
473
|
+
destructiveHint: false,
|
|
474
|
+
idempotentHint: true,
|
|
475
|
+
openWorldHint: false,
|
|
476
|
+
agentHidden: false,
|
|
477
|
+
},
|
|
478
|
+
description: 'Load a named SKILL.md into context.',
|
|
479
|
+
inputSchema: {
|
|
480
|
+
type: 'object',
|
|
481
|
+
properties: {
|
|
482
|
+
name: { type: 'string', description: 'Skill name.' },
|
|
483
|
+
},
|
|
484
|
+
required: ['name'],
|
|
485
|
+
additionalProperties: false,
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
|
|
383
489
|
const MEASURED_TOOL_USAGE = Object.freeze({
|
|
384
490
|
read: 710,
|
|
385
491
|
code_graph: 520,
|
|
386
492
|
grep: 500,
|
|
493
|
+
find: 480,
|
|
387
494
|
glob: 460,
|
|
388
495
|
list: 430,
|
|
389
496
|
apply_patch: 400,
|
|
390
497
|
explore: 360,
|
|
391
|
-
|
|
498
|
+
agent: 330,
|
|
392
499
|
shell: 81,
|
|
393
500
|
cwd: 2,
|
|
394
501
|
diagnostics: 2,
|
|
@@ -399,44 +506,54 @@ const MEASURED_TOOL_USAGE = Object.freeze({
|
|
|
399
506
|
channel_status: 2,
|
|
400
507
|
});
|
|
401
508
|
const MEASURED_TOOL_ORDER = Object.freeze(Object.keys(MEASURED_TOOL_USAGE));
|
|
402
|
-
const
|
|
509
|
+
const LEAD_DISALLOWED_TOOLS = Object.freeze(['diagnostics', 'open_config']);
|
|
510
|
+
const DEFERRED_DEFAULT_FULL_TOOLS = Object.freeze([
|
|
511
|
+
'read',
|
|
512
|
+
'code_graph',
|
|
513
|
+
'grep',
|
|
514
|
+
'find',
|
|
515
|
+
'glob',
|
|
516
|
+
'list',
|
|
517
|
+
'explore',
|
|
518
|
+
'apply_patch',
|
|
519
|
+
'Skill',
|
|
403
520
|
'tool_search',
|
|
404
|
-
'recall',
|
|
405
|
-
'search',
|
|
406
|
-
'web_fetch',
|
|
407
521
|
]);
|
|
408
|
-
const LEAD_DISALLOWED_TOOLS = Object.freeze(['diagnostics', 'open_config']);
|
|
409
|
-
const DEFERRED_DEFAULT_FULL_LIMIT = 9;
|
|
410
522
|
const DEFERRED_DEFAULT_READONLY_TOOLS = Object.freeze([
|
|
411
523
|
'read',
|
|
412
524
|
'code_graph',
|
|
413
525
|
'grep',
|
|
526
|
+
'find',
|
|
414
527
|
'glob',
|
|
415
528
|
'list',
|
|
416
529
|
'explore',
|
|
530
|
+
'Skill',
|
|
417
531
|
'tool_search',
|
|
418
532
|
]);
|
|
419
533
|
const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
|
|
420
534
|
'read',
|
|
421
535
|
'code_graph',
|
|
422
536
|
'grep',
|
|
537
|
+
'find',
|
|
423
538
|
'glob',
|
|
424
539
|
'list',
|
|
425
540
|
'shell',
|
|
426
541
|
'task',
|
|
427
542
|
'explore',
|
|
428
543
|
'apply_patch',
|
|
429
|
-
'
|
|
544
|
+
'agent',
|
|
430
545
|
'recall',
|
|
431
546
|
'search',
|
|
432
547
|
'web_fetch',
|
|
433
548
|
'cwd',
|
|
549
|
+
'Skill',
|
|
434
550
|
'tool_search',
|
|
435
551
|
]);
|
|
436
552
|
const READONLY_TOOL_NAMES = new Set([
|
|
437
553
|
'read',
|
|
438
554
|
'list',
|
|
439
555
|
'grep',
|
|
556
|
+
'find',
|
|
440
557
|
'glob',
|
|
441
558
|
'code_graph',
|
|
442
559
|
'search',
|
|
@@ -447,21 +564,22 @@ const READONLY_TOOL_NAMES = new Set([
|
|
|
447
564
|
'channel_status',
|
|
448
565
|
'schedule_status',
|
|
449
566
|
'fetch',
|
|
567
|
+
'Skill',
|
|
450
568
|
]);
|
|
451
|
-
const
|
|
569
|
+
const AGENT_HIDDEN_WRAPPER_TOOLS = new Set(['search']);
|
|
452
570
|
|
|
453
571
|
function applyStandaloneToolDefaults(tool) {
|
|
454
|
-
if (!tool || !
|
|
572
|
+
if (!tool || !AGENT_HIDDEN_WRAPPER_TOOLS.has(tool.name)) return tool;
|
|
455
573
|
return {
|
|
456
574
|
...tool,
|
|
457
575
|
annotations: {
|
|
458
576
|
...(tool.annotations || {}),
|
|
459
|
-
|
|
577
|
+
agentHidden: true,
|
|
460
578
|
},
|
|
461
579
|
};
|
|
462
580
|
}
|
|
463
581
|
const DEFERRED_SELECT_ALIASES = {
|
|
464
|
-
filesystem: ['read', 'list', 'grep', 'glob'],
|
|
582
|
+
filesystem: ['read', 'list', 'grep', 'find', 'glob'],
|
|
465
583
|
search: ['search', 'web_fetch'],
|
|
466
584
|
web: ['web_fetch', 'search'],
|
|
467
585
|
memory: ['memory', 'recall'],
|
|
@@ -474,11 +592,68 @@ const DEFERRED_SELECT_ALIASES = {
|
|
|
474
592
|
channel: ['channel_status'],
|
|
475
593
|
explore: ['explore'],
|
|
476
594
|
discovery: ['explore'],
|
|
477
|
-
|
|
595
|
+
agent: ['agent'],
|
|
478
596
|
graph: ['code_graph'],
|
|
479
597
|
code: ['code_graph'],
|
|
480
598
|
shell: ['shell', 'task'],
|
|
481
599
|
};
|
|
600
|
+
const TOOL_SEARCH_SAFE_AUTO_ALIASES = new Set([
|
|
601
|
+
'shell',
|
|
602
|
+
'web',
|
|
603
|
+
'search',
|
|
604
|
+
'agent',
|
|
605
|
+
'provider',
|
|
606
|
+
'providers',
|
|
607
|
+
'channel',
|
|
608
|
+
'schedule',
|
|
609
|
+
'memory',
|
|
610
|
+
]);
|
|
611
|
+
const TOOL_SEARCH_AMBIGUOUS_AUTO_QUERIES = new Set([
|
|
612
|
+
'status',
|
|
613
|
+
'state',
|
|
614
|
+
'info',
|
|
615
|
+
'list',
|
|
616
|
+
'show',
|
|
617
|
+
'config',
|
|
618
|
+
]);
|
|
619
|
+
const TOOL_SEARCH_STOP_WORDS = new Set([
|
|
620
|
+
'a',
|
|
621
|
+
'an',
|
|
622
|
+
'and',
|
|
623
|
+
'are',
|
|
624
|
+
'as',
|
|
625
|
+
'for',
|
|
626
|
+
'from',
|
|
627
|
+
'how',
|
|
628
|
+
'i',
|
|
629
|
+
'in',
|
|
630
|
+
'is',
|
|
631
|
+
'me',
|
|
632
|
+
'need',
|
|
633
|
+
'of',
|
|
634
|
+
'on',
|
|
635
|
+
'please',
|
|
636
|
+
'the',
|
|
637
|
+
'to',
|
|
638
|
+
'tool',
|
|
639
|
+
'tools',
|
|
640
|
+
'use',
|
|
641
|
+
'using',
|
|
642
|
+
'with',
|
|
643
|
+
]);
|
|
644
|
+
const TOOL_SEARCH_ROW_ALIASES = Object.freeze({
|
|
645
|
+
agent: ['delegate', 'subagent', 'worker', 'parallel agent', 'background agent', 'reviewer', 'explorer'],
|
|
646
|
+
channel_status: ['channel status', 'discord status', 'channel config'],
|
|
647
|
+
cwd: ['cwd', 'working directory', 'current directory', 'project root', 'folder'],
|
|
648
|
+
memory: ['save memory', 'store memory', 'delete memory', 'forget memory', 'memory status'],
|
|
649
|
+
provider_status: ['provider status', 'auth status', 'model status', 'oauth status', 'provider config'],
|
|
650
|
+
recall: ['recall', 'previous work', 'past work', 'prior context', 'history', 'resume context'],
|
|
651
|
+
schedule_status: ['schedule status', 'cron status'],
|
|
652
|
+
search: ['web search', 'internet search', 'current info', 'latest info', 'online search', 'docs search'],
|
|
653
|
+
shell: ['run command', 'execute command', 'terminal', 'powershell', 'bash', 'run tests', 'test command', 'build command', 'npm', 'node', 'git'],
|
|
654
|
+
task: ['background task', 'async task', 'wait task', 'cancel task', 'task status'],
|
|
655
|
+
web_fetch: ['fetch url', 'fetch page', 'open url', 'web page', 'read url', 'docs page'],
|
|
656
|
+
});
|
|
482
657
|
|
|
483
658
|
function normalizeToolMode(mode) {
|
|
484
659
|
const value = String(mode || '').trim().toLowerCase();
|
|
@@ -503,8 +678,21 @@ function effortOptionsFor(provider, model) {
|
|
|
503
678
|
const declared = Array.isArray(model?.reasoningLevels)
|
|
504
679
|
? model.reasoningLevels.map(clean).filter(Boolean)
|
|
505
680
|
: [];
|
|
506
|
-
if (Array.isArray(model?.reasoningLevels)) return filterProvider(declared);
|
|
507
681
|
const family = clean(model?.family).toLowerCase();
|
|
682
|
+
if (Array.isArray(model?.reasoningLevels)) {
|
|
683
|
+
if (declared.length) return filterProvider(declared);
|
|
684
|
+
if (Object.prototype.hasOwnProperty.call(EFFORT_BY_FAMILY, family)) {
|
|
685
|
+
return filterProvider(EFFORT_BY_FAMILY[family]);
|
|
686
|
+
}
|
|
687
|
+
return [];
|
|
688
|
+
}
|
|
689
|
+
const reasoningOptionEffort = Array.isArray(model?.reasoningOptions)
|
|
690
|
+
? model.reasoningOptions.find((option) => clean(option?.type).toLowerCase() === 'effort')
|
|
691
|
+
: null;
|
|
692
|
+
const reasoningOptionValues = Array.isArray(reasoningOptionEffort?.values)
|
|
693
|
+
? reasoningOptionEffort.values.map(clean).filter(Boolean)
|
|
694
|
+
: [];
|
|
695
|
+
if (reasoningOptionValues.length) return filterProvider(reasoningOptionValues);
|
|
508
696
|
if (Object.prototype.hasOwnProperty.call(EFFORT_BY_FAMILY, family)) {
|
|
509
697
|
return filterProvider(EFFORT_BY_FAMILY[family]);
|
|
510
698
|
}
|
|
@@ -557,10 +745,23 @@ function toolSpecForMode(mode) {
|
|
|
557
745
|
return mode === 'readonly' ? ['tools:readonly'] : 'full';
|
|
558
746
|
}
|
|
559
747
|
|
|
748
|
+
function deferredSurfaceModeForLead(mode) {
|
|
749
|
+
return mode === 'readonly' ? 'readonly' : 'lead';
|
|
750
|
+
}
|
|
751
|
+
|
|
560
752
|
function clean(value) {
|
|
561
753
|
return String(value ?? '').trim();
|
|
562
754
|
}
|
|
563
755
|
|
|
756
|
+
// A resolved model meta carries catalog-derived fields (contextWindow, pricing,
|
|
757
|
+
// capabilities, …). The lookupModelMeta() fallback for an unknown id is the
|
|
758
|
+
// bare shape `{ id, provider }`, so "more than id/provider" reliably tells a
|
|
759
|
+
// real catalog hit apart from that placeholder.
|
|
760
|
+
function modelMetaLooksResolved(meta) {
|
|
761
|
+
if (!meta || typeof meta !== 'object') return false;
|
|
762
|
+
return Object.keys(meta).some((key) => key !== 'id' && key !== 'provider');
|
|
763
|
+
}
|
|
764
|
+
|
|
564
765
|
const OUTPUT_STYLE_ORDER = ['default', 'simple', 'extreme-simple'];
|
|
565
766
|
const OUTPUT_STYLE_ALIASES = new Map([
|
|
566
767
|
['compact', 'default'],
|
|
@@ -793,6 +994,21 @@ function resolveRoute(config, { provider, model, effort, fast } = {}) {
|
|
|
793
994
|
};
|
|
794
995
|
}
|
|
795
996
|
|
|
997
|
+
function isLikelyRawModelId(value) {
|
|
998
|
+
const model = clean(value);
|
|
999
|
+
if (!model || model.length > 160) return false;
|
|
1000
|
+
if (/\s/.test(model)) return false;
|
|
1001
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:/@+-]*$/.test(model);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function validateRequestedModelSelector(config, requested = {}) {
|
|
1005
|
+
const model = clean(requested.model);
|
|
1006
|
+
if (!model) return;
|
|
1007
|
+
if (findPreset(config, model)) return;
|
|
1008
|
+
if (isLikelyRawModelId(model)) return;
|
|
1009
|
+
throw new Error(`Invalid model selector "${model}". Use a preset or a model id; free-form text cannot be used as a model.`);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
796
1012
|
function ensureProviderEnabled(config, provider) {
|
|
797
1013
|
const providers = { ...(config?.providers || {}) };
|
|
798
1014
|
providers[provider] = { ...(providers[provider] || {}), enabled: true };
|
|
@@ -827,12 +1043,51 @@ function normalizeSystemShellCommand(value) {
|
|
|
827
1043
|
function normalizeAutoClearConfig(value = {}) {
|
|
828
1044
|
const raw = value && typeof value === 'object' ? value : {};
|
|
829
1045
|
const idleMs = Number(raw.idleMs ?? raw.thresholdMs ?? raw.idleMillis);
|
|
1046
|
+
const compactType = clean(raw.compactType ?? raw.compact_type ?? raw.type);
|
|
1047
|
+
const normalizedCompactType = compactType ? normalizeCompactTypeSetting(compactType, 'semantic') : '';
|
|
830
1048
|
return {
|
|
831
1049
|
enabled: raw.enabled !== false,
|
|
832
1050
|
idleMs: Number.isFinite(idleMs) && idleMs > 0 ? Math.max(60_000, Math.round(idleMs)) : AUTO_CLEAR_DEFAULT_IDLE_MS,
|
|
1051
|
+
...(normalizedCompactType ? { compactType: normalizedCompactType } : {}),
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function normalizeCompactTypeSetting(value, fallback = 'semantic') {
|
|
1056
|
+
const raw = clean(value).toLowerCase().replace(/_/g, '-');
|
|
1057
|
+
if (!raw) return fallback;
|
|
1058
|
+
if (raw === '1' || raw === 'type1' || raw === 'type-1' || raw === 'semantic' || raw === 'summary' || raw === 'default') return 'semantic';
|
|
1059
|
+
if (raw === '2' || raw === 'type2' || raw === 'type-2' || raw === 'recall' || raw === 'recall-fast' || raw === 'recall-fasttrack' || raw === 'recall-fast-track' || raw === 'fasttrack' || raw === 'fast-track') return 'recall-fasttrack';
|
|
1060
|
+
return fallback;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function normalizeCompactionConfig(value = {}, { memoryEnabled = true } = {}) {
|
|
1064
|
+
const raw = value && typeof value === 'object' ? value : {};
|
|
1065
|
+
let compactType = normalizeCompactTypeSetting(raw.compactType ?? raw.compact_type ?? raw.type, 'semantic');
|
|
1066
|
+
if (compactType === 'recall-fasttrack' && memoryEnabled === false) compactType = 'semantic';
|
|
1067
|
+
return {
|
|
1068
|
+
...raw,
|
|
1069
|
+
auto: raw.auto !== false && raw.enabled !== false,
|
|
1070
|
+
type: compactType,
|
|
1071
|
+
compactType,
|
|
833
1072
|
};
|
|
834
1073
|
}
|
|
835
1074
|
|
|
1075
|
+
function moduleEnabled(configLike, name, fallback = true) {
|
|
1076
|
+
const entry = configLike?.modules?.[name];
|
|
1077
|
+
if (entry && typeof entry === 'object' && entry.enabled === false) return false;
|
|
1078
|
+
return fallback !== false;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function setModuleEnabledInConfig(configLike, name, enabled) {
|
|
1082
|
+
const next = { ...(configLike || {}) };
|
|
1083
|
+
next.modules = { ...(next.modules || {}) };
|
|
1084
|
+
next.modules[name] = {
|
|
1085
|
+
...(next.modules[name] || {}),
|
|
1086
|
+
enabled: enabled !== false,
|
|
1087
|
+
};
|
|
1088
|
+
return next;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
836
1091
|
function formatDurationMs(ms) {
|
|
837
1092
|
const value = Math.max(0, Number(ms) || 0);
|
|
838
1093
|
if (value % 3_600_000 === 0) return `${value / 3_600_000}h`;
|
|
@@ -881,6 +1136,44 @@ function openAiDirectModelSupportsFast(model) {
|
|
|
881
1136
|
|| /^gpt-5\.4-mini(?:-\d{4}|$)/.test(id);
|
|
882
1137
|
}
|
|
883
1138
|
|
|
1139
|
+
function openAiModelSupportsHostedWebSearch(model) {
|
|
1140
|
+
const id = clean(model?.id || model).toLowerCase();
|
|
1141
|
+
if (!id) return false;
|
|
1142
|
+
if (model?.supportsWebSearch === true) return true;
|
|
1143
|
+
const tools = [
|
|
1144
|
+
...(Array.isArray(model?.supportedTools) ? model.supportedTools : []),
|
|
1145
|
+
...(Array.isArray(model?.tools) ? model.tools : []),
|
|
1146
|
+
...(Array.isArray(model?.capabilities?.tools) ? model.capabilities.tools : []),
|
|
1147
|
+
].map((tool) => clean(tool?.type || tool?.name || tool).toLowerCase());
|
|
1148
|
+
if (tools.some((tool) => tool === 'web_search' || tool === 'web_search_preview')) return true;
|
|
1149
|
+
if (/codex|image|audio|tts|stt|embedding|rerank|moderation|search-preview/.test(id)) return false;
|
|
1150
|
+
return /^gpt-(5(?:\.|$|-)|4\.1(?:-|$)|4o(?:-|$)|4\.5(?:-|$))/.test(id)
|
|
1151
|
+
|| /^o[34](?:-|$)/.test(id);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function grokModelSupportsHostedWebSearch(model) {
|
|
1155
|
+
const id = clean(model?.id || model).toLowerCase();
|
|
1156
|
+
if (!id || /imagine|image|video|composer/.test(id)) return false;
|
|
1157
|
+
if (id === 'grok-build') return false;
|
|
1158
|
+
return /^grok-/.test(id);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function geminiModelSupportsHostedWebSearch(model) {
|
|
1162
|
+
const id = clean(model?.id || model).toLowerCase();
|
|
1163
|
+
if (!id || /embedding|aqa|imagen|veo|tts|image|computer-use|customtools/.test(id)) return false;
|
|
1164
|
+
return /^gemini-(3(?:\.|-|$)|2\.5-|2\.0-flash)/.test(id);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function anthropicModelSupportsHostedWebSearch(model) {
|
|
1168
|
+
const id = clean(model?.id || model).toLowerCase();
|
|
1169
|
+
if (!id) return false;
|
|
1170
|
+
const match = id.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:[-.](\d+))?/);
|
|
1171
|
+
if (!match) return false;
|
|
1172
|
+
const major = Number(match[2]) || 0;
|
|
1173
|
+
const minor = Number(match[3]) || 0;
|
|
1174
|
+
return major > 4 || (major === 4 && minor >= 0);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
884
1177
|
function anthropicModelMetaSupportsFast(model) {
|
|
885
1178
|
const id = clean(model?.id || model).toLowerCase();
|
|
886
1179
|
return /^claude-(opus|sonnet)/.test(id);
|
|
@@ -895,6 +1188,16 @@ function fastCapableFor(provider, model) {
|
|
|
895
1188
|
return false;
|
|
896
1189
|
}
|
|
897
1190
|
|
|
1191
|
+
function searchCapableFor(provider, model) {
|
|
1192
|
+
const p = normalizeSearchProviderId(provider);
|
|
1193
|
+
if (!isSearchCapableProvider(p)) return false;
|
|
1194
|
+
if (p === 'openai' || p === 'openai-oauth') return openAiModelSupportsHostedWebSearch(model);
|
|
1195
|
+
if (p === 'grok-oauth' || p === 'xai') return grokModelSupportsHostedWebSearch(model);
|
|
1196
|
+
if (p === 'gemini') return geminiModelSupportsHostedWebSearch(model);
|
|
1197
|
+
if (p === 'anthropic' || p === 'anthropic-oauth') return anthropicModelSupportsHostedWebSearch(model);
|
|
1198
|
+
return model?.supportsWebSearch === true;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
898
1201
|
function fastPreferenceFor(config, provider, model) {
|
|
899
1202
|
const key = routeFastKey(provider, model);
|
|
900
1203
|
if (!key) return false;
|
|
@@ -903,10 +1206,10 @@ function fastPreferenceFor(config, provider, model) {
|
|
|
903
1206
|
return config?.fastModels?.[key] === true;
|
|
904
1207
|
}
|
|
905
1208
|
|
|
906
|
-
function saveModelSettings(cfgMod, route, { fastCapable = true } = {}) {
|
|
1209
|
+
function saveModelSettings(cfgMod, route, { fastCapable = true, baseConfig = null } = {}) {
|
|
907
1210
|
const key = routeFastKey(route?.provider, route?.model);
|
|
908
|
-
if (!key) return cfgMod.loadConfig();
|
|
909
|
-
const nextConfig = cfgMod.loadConfig();
|
|
1211
|
+
if (!key) return baseConfig || cfgMod.loadConfig();
|
|
1212
|
+
const nextConfig = baseConfig || cfgMod.loadConfig();
|
|
910
1213
|
const modelSettings = { ...(nextConfig.modelSettings || {}) };
|
|
911
1214
|
const nextSetting = { ...(modelSettings[key] || {}) };
|
|
912
1215
|
if (hasOwn(route, 'effort') && route.effort) nextSetting.effort = route.effort;
|
|
@@ -948,17 +1251,80 @@ function routeForStatusline(route) {
|
|
|
948
1251
|
return out;
|
|
949
1252
|
}
|
|
950
1253
|
|
|
1254
|
+
function writeStatuslineRoute(statusRoutes, session, route) {
|
|
1255
|
+
if (!session?.id || !route) return;
|
|
1256
|
+
const clientHostPid = session?.clientHostPid || process.pid;
|
|
1257
|
+
statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route), { clientHostPid });
|
|
1258
|
+
}
|
|
1259
|
+
|
|
951
1260
|
const ONBOARDING_VERSION = 1;
|
|
952
|
-
const WORKFLOW_ROUTE_SLOTS = ['lead', '
|
|
1261
|
+
const WORKFLOW_ROUTE_SLOTS = ['lead', 'agent', 'explorer', 'memory'];
|
|
953
1262
|
const FIXED_AGENT_SLOTS = Object.freeze([
|
|
954
1263
|
{ id: 'explore', label: 'Explore', description: 'Broad repository exploration', workflowSlot: 'explorer' },
|
|
955
|
-
{ id: 'web-researcher', label: 'Web Researcher', description: 'External current-info research' },
|
|
956
1264
|
{ id: 'maintainer', label: 'Maintainer', description: 'Background memory and upkeep', workflowSlot: 'memory' },
|
|
957
1265
|
{ id: 'worker', label: 'Worker', description: 'Scoped implementation' },
|
|
958
1266
|
{ id: 'heavy-worker', label: 'Heavy Worker', description: 'Broad or multi-file implementation' },
|
|
959
1267
|
{ id: 'reviewer', label: 'Reviewer', description: 'Diff review and risk checks' },
|
|
960
1268
|
{ id: 'debugger', label: 'Debugger', description: 'Root-cause analysis and failure tracing' },
|
|
961
1269
|
]);
|
|
1270
|
+
const SEARCH_CAPABLE_PROVIDERS = new Set([
|
|
1271
|
+
'openai-oauth',
|
|
1272
|
+
'openai',
|
|
1273
|
+
'grok-oauth',
|
|
1274
|
+
'xai',
|
|
1275
|
+
'gemini',
|
|
1276
|
+
'anthropic',
|
|
1277
|
+
'anthropic-oauth',
|
|
1278
|
+
]);
|
|
1279
|
+
const SEARCH_DEFAULT_PROVIDER = 'default';
|
|
1280
|
+
const SEARCH_DEFAULT_MODEL = 'default';
|
|
1281
|
+
const SEARCH_PROVIDER_ALIASES = Object.freeze({
|
|
1282
|
+
'openai-api': 'openai',
|
|
1283
|
+
'xai-api': 'xai',
|
|
1284
|
+
'gemini-api': 'gemini',
|
|
1285
|
+
'anthropic-api': 'anthropic',
|
|
1286
|
+
});
|
|
1287
|
+
const QUICK_SEARCH_MODELS = Object.freeze({
|
|
1288
|
+
'openai-oauth': [
|
|
1289
|
+
{ id: 'gpt-5.5', display: 'GPT-5.5', latest: true, contextWindow: 1000000 },
|
|
1290
|
+
{ id: 'gpt-5.4', display: 'GPT-5.4', latest: true, contextWindow: 1000000 },
|
|
1291
|
+
{ id: 'gpt-5', display: 'GPT-5', contextWindow: 400000 },
|
|
1292
|
+
{ id: 'gpt-4.1', display: 'GPT-4.1', contextWindow: 1000000 },
|
|
1293
|
+
],
|
|
1294
|
+
openai: [
|
|
1295
|
+
{ id: 'gpt-5.5', display: 'GPT-5.5', latest: true, contextWindow: 1000000 },
|
|
1296
|
+
{ id: 'gpt-5.4', display: 'GPT-5.4', latest: true, contextWindow: 1000000 },
|
|
1297
|
+
{ id: 'gpt-5', display: 'GPT-5', contextWindow: 400000 },
|
|
1298
|
+
{ id: 'gpt-4.1', display: 'GPT-4.1', contextWindow: 1000000 },
|
|
1299
|
+
{ id: 'gpt-4o', display: 'GPT-4o', contextWindow: 128000 },
|
|
1300
|
+
],
|
|
1301
|
+
'grok-oauth': [
|
|
1302
|
+
{ id: 'grok-4.3', display: 'Grok 4.3', latest: true, contextWindow: 1000000 },
|
|
1303
|
+
{ id: 'grok-4.20', display: 'Grok 4.20', contextWindow: 1000000 },
|
|
1304
|
+
{ id: 'grok-4', display: 'Grok 4', contextWindow: 256000 },
|
|
1305
|
+
],
|
|
1306
|
+
xai: [
|
|
1307
|
+
{ id: 'grok-4.3', display: 'Grok 4.3', latest: true, contextWindow: 1000000 },
|
|
1308
|
+
{ id: 'grok-4.20', display: 'Grok 4.20', contextWindow: 1000000 },
|
|
1309
|
+
{ id: 'grok-4', display: 'Grok 4', contextWindow: 256000 },
|
|
1310
|
+
],
|
|
1311
|
+
gemini: [
|
|
1312
|
+
{ id: 'gemini-3-pro', display: 'Gemini 3 Pro', latest: true, contextWindow: 1000000 },
|
|
1313
|
+
{ id: 'gemini-2.5-pro', display: 'Gemini 2.5 Pro', contextWindow: 1000000 },
|
|
1314
|
+
{ id: 'gemini-2.5-flash', display: 'Gemini 2.5 Flash', contextWindow: 1000000 },
|
|
1315
|
+
{ id: 'gemini-2.0-flash', display: 'Gemini 2.0 Flash', contextWindow: 1000000 },
|
|
1316
|
+
],
|
|
1317
|
+
'anthropic-oauth': [
|
|
1318
|
+
{ id: 'claude-opus-4-8', display: 'Claude Opus 4.8', latest: true, contextWindow: 1000000 },
|
|
1319
|
+
{ id: 'claude-sonnet-4-6', display: 'Claude Sonnet 4.6', latest: true, contextWindow: 1000000 },
|
|
1320
|
+
{ id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5', contextWindow: 200000 },
|
|
1321
|
+
],
|
|
1322
|
+
anthropic: [
|
|
1323
|
+
{ id: 'claude-opus-4-8', display: 'Claude Opus 4.8', latest: true, contextWindow: 1000000 },
|
|
1324
|
+
{ id: 'claude-sonnet-4-6', display: 'Claude Sonnet 4.6', latest: true, contextWindow: 1000000 },
|
|
1325
|
+
{ id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5', contextWindow: 200000 },
|
|
1326
|
+
],
|
|
1327
|
+
});
|
|
962
1328
|
const AGENT_ROLE_IDS = new Set(FIXED_AGENT_SLOTS.map((agent) => agent.id));
|
|
963
1329
|
const agentDefinitionCache = new Map();
|
|
964
1330
|
const DEFAULT_WORKFLOW_ID = 'default';
|
|
@@ -1016,12 +1382,14 @@ function readWorkflowPackFromDir(dir, source = 'built-in') {
|
|
|
1016
1382
|
const entry = clean(manifest.entry) || 'WORKFLOW.md';
|
|
1017
1383
|
const body = readTextSafe(join(dir, entry));
|
|
1018
1384
|
if (!body) return null;
|
|
1385
|
+
const agentsConfigured = Array.isArray(manifest.agents);
|
|
1019
1386
|
return {
|
|
1020
1387
|
id,
|
|
1021
1388
|
name: clean(manifest.name) || id,
|
|
1022
1389
|
description: clean(manifest.description),
|
|
1023
1390
|
entry,
|
|
1024
|
-
|
|
1391
|
+
agentsConfigured,
|
|
1392
|
+
agents: agentsConfigured ? manifest.agents.map((agent) => normalizeAgentId(agent) || normalizeWorkflowId(agent)).filter(Boolean) : [],
|
|
1025
1393
|
body,
|
|
1026
1394
|
source,
|
|
1027
1395
|
};
|
|
@@ -1059,6 +1427,20 @@ function loadWorkflowPack(dataDir, id) {
|
|
|
1059
1427
|
return readWorkflowPackFromDir(join(STANDALONE_ROOT, 'workflows', DEFAULT_WORKFLOW_ID), 'built-in');
|
|
1060
1428
|
}
|
|
1061
1429
|
|
|
1430
|
+
function workflowSummary(pack) {
|
|
1431
|
+
const id = normalizeWorkflowId(pack?.id, DEFAULT_WORKFLOW_ID);
|
|
1432
|
+
return {
|
|
1433
|
+
id,
|
|
1434
|
+
name: clean(pack?.name) || (id === DEFAULT_WORKFLOW_ID ? 'Default' : id),
|
|
1435
|
+
description: clean(pack?.description),
|
|
1436
|
+
source: clean(pack?.source),
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function activeWorkflowSummary(config, dataDir) {
|
|
1441
|
+
return workflowSummary(loadWorkflowPack(dataDir, activeWorkflowId(config)));
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1062
1444
|
function loadAgentDefinition(dataDir, id) {
|
|
1063
1445
|
const agentId = normalizeAgentId(id) || normalizeWorkflowId(id);
|
|
1064
1446
|
if (!agentId) return null;
|
|
@@ -1067,20 +1449,22 @@ function loadAgentDefinition(dataDir, id) {
|
|
|
1067
1449
|
for (const dir of agentSourceDirs(dataDir, agentId)) {
|
|
1068
1450
|
const manifest = readJsonSafe(join(dir, 'agent.json')) || {};
|
|
1069
1451
|
const entry = clean(manifest.entry) || 'AGENT.md';
|
|
1070
|
-
const
|
|
1452
|
+
const doc = readMarkdownDocument(readTextSafe(join(dir, entry)));
|
|
1453
|
+
const body = doc.body;
|
|
1071
1454
|
if (!body) continue;
|
|
1072
1455
|
const definition = {
|
|
1073
1456
|
id: agentId,
|
|
1074
1457
|
name: clean(manifest.name) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
|
|
1075
1458
|
description: clean(manifest.description) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.description || '',
|
|
1459
|
+
permission: normalizeAgentPermissionOrNone(doc.frontmatter.permission),
|
|
1460
|
+
frontmatter: doc.frontmatter,
|
|
1076
1461
|
body,
|
|
1077
1462
|
};
|
|
1078
1463
|
agentDefinitionCache.set(cacheKey, definition);
|
|
1079
1464
|
return definition;
|
|
1080
1465
|
}
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
if (!legacyBody) {
|
|
1466
|
+
const legacyDoc = readMarkdownDocument(readTextSafe(join(STANDALONE_ROOT, 'agents', `${agentId}.md`)));
|
|
1467
|
+
if (!legacyDoc.body) {
|
|
1084
1468
|
agentDefinitionCache.set(cacheKey, null);
|
|
1085
1469
|
return null;
|
|
1086
1470
|
}
|
|
@@ -1088,7 +1472,9 @@ function loadAgentDefinition(dataDir, id) {
|
|
|
1088
1472
|
id: agentId,
|
|
1089
1473
|
name: FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
|
|
1090
1474
|
description: '',
|
|
1091
|
-
|
|
1475
|
+
permission: normalizeAgentPermissionOrNone(legacyDoc.frontmatter.permission),
|
|
1476
|
+
frontmatter: legacyDoc.frontmatter,
|
|
1477
|
+
body: legacyDoc.body,
|
|
1092
1478
|
};
|
|
1093
1479
|
agentDefinitionCache.set(cacheKey, definition);
|
|
1094
1480
|
return definition;
|
|
@@ -1103,7 +1489,7 @@ function workflowContextBlock(config, dataDir) {
|
|
|
1103
1489
|
if (pack.description) lines.push(pack.description);
|
|
1104
1490
|
lines.push(pack.body);
|
|
1105
1491
|
|
|
1106
|
-
const agentIds = pack.
|
|
1492
|
+
const agentIds = pack.agentsConfigured ? pack.agents : FIXED_AGENT_SLOTS.map((agent) => agent.id);
|
|
1107
1493
|
const agentBlocks = agentIds
|
|
1108
1494
|
.map((id) => loadAgentDefinition(dataDir, id))
|
|
1109
1495
|
.filter(Boolean);
|
|
@@ -1122,6 +1508,11 @@ function normalizeWorkflowRoute(routeLike, fallback = {}) {
|
|
|
1122
1508
|
const provider = clean(routeLike?.provider) || clean(fallback.provider);
|
|
1123
1509
|
const model = clean(routeLike?.model) || clean(fallback.model);
|
|
1124
1510
|
if (!provider || !model) return null;
|
|
1511
|
+
// Defensive: a workflow/agent route must carry a real model id. Reject values
|
|
1512
|
+
// that are obviously free-form text (whitespace, prose) so a bad string can
|
|
1513
|
+
// never be persisted as a preset/workflow route. Normal model ids pass
|
|
1514
|
+
// isLikelyRawModelId unchanged.
|
|
1515
|
+
if (!isLikelyRawModelId(model)) return null;
|
|
1125
1516
|
const effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
|
|
1126
1517
|
const fast = routeLike?.fast ?? fallback.fast;
|
|
1127
1518
|
return {
|
|
@@ -1132,6 +1523,41 @@ function normalizeWorkflowRoute(routeLike, fallback = {}) {
|
|
|
1132
1523
|
};
|
|
1133
1524
|
}
|
|
1134
1525
|
|
|
1526
|
+
function normalizeSearchProviderId(provider) {
|
|
1527
|
+
const id = clean(provider);
|
|
1528
|
+
return SEARCH_PROVIDER_ALIASES[id] || id;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function isDefaultSearchRouteConfig(routeLike = {}) {
|
|
1532
|
+
return normalizeSearchProviderId(routeLike?.provider) === SEARCH_DEFAULT_PROVIDER
|
|
1533
|
+
&& clean(routeLike?.model).toLowerCase() === SEARCH_DEFAULT_MODEL;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function isSearchCapableProvider(provider) {
|
|
1537
|
+
return SEARCH_CAPABLE_PROVIDERS.has(normalizeSearchProviderId(provider));
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function normalizeSearchRouteConfig(routeLike, fallback = {}) {
|
|
1541
|
+
const provider = normalizeSearchProviderId(routeLike?.provider || fallback.provider);
|
|
1542
|
+
const model = clean(routeLike?.model || fallback.model);
|
|
1543
|
+
if (!provider || !model) return null;
|
|
1544
|
+
let effort = null;
|
|
1545
|
+
try {
|
|
1546
|
+
effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
|
|
1547
|
+
} catch {
|
|
1548
|
+
effort = null;
|
|
1549
|
+
}
|
|
1550
|
+
const fast = routeLike?.fast ?? fallback.fast;
|
|
1551
|
+
const toolType = clean(routeLike?.toolType || fallback.toolType);
|
|
1552
|
+
return {
|
|
1553
|
+
provider,
|
|
1554
|
+
model,
|
|
1555
|
+
...(effort ? { effort } : {}),
|
|
1556
|
+
...(fast === true ? { fast: true } : {}),
|
|
1557
|
+
...(toolType ? { toolType } : {}),
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1135
1561
|
function upsertWorkflowPreset(presets, slot, routeLike) {
|
|
1136
1562
|
const route = normalizeWorkflowRoute(routeLike);
|
|
1137
1563
|
if (!route) return presets;
|
|
@@ -1139,7 +1565,7 @@ function upsertWorkflowPreset(presets, slot, routeLike) {
|
|
|
1139
1565
|
const preset = {
|
|
1140
1566
|
id,
|
|
1141
1567
|
name: workflowPresetName(slot),
|
|
1142
|
-
type: '
|
|
1568
|
+
type: 'agent',
|
|
1143
1569
|
provider: route.provider,
|
|
1144
1570
|
model: route.model,
|
|
1145
1571
|
...(route.effort ? { effort: route.effort } : {}),
|
|
@@ -1161,24 +1587,19 @@ function summarizeWorkflowRoutes(config) {
|
|
|
1161
1587
|
return out;
|
|
1162
1588
|
}
|
|
1163
1589
|
|
|
1164
|
-
function
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
} catch {
|
|
1172
|
-
return '';
|
|
1590
|
+
function routeFromPreset(config, slotValue) {
|
|
1591
|
+
// Maintenance slots now store a direct {provider, model} route. Accept that
|
|
1592
|
+
// shape first; fall back to the legacy preset-NAME string lookup so configs
|
|
1593
|
+
// written before the route migration still resolve.
|
|
1594
|
+
if (slotValue && typeof slotValue === 'object' && !Array.isArray(slotValue)) {
|
|
1595
|
+
const direct = normalizeWorkflowRoute(slotValue);
|
|
1596
|
+
if (direct) return direct;
|
|
1173
1597
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
function routeFromPreset(config, presetName) {
|
|
1177
|
-
const preset = findPreset(config, presetName);
|
|
1598
|
+
const preset = findPreset(config, slotValue);
|
|
1178
1599
|
return preset ? normalizeWorkflowRoute(preset) : null;
|
|
1179
1600
|
}
|
|
1180
1601
|
|
|
1181
|
-
function agentRouteFromConfig(config, agentId,
|
|
1602
|
+
function agentRouteFromConfig(config, agentId, _dataDir) {
|
|
1182
1603
|
const id = normalizeAgentId(agentId);
|
|
1183
1604
|
if (!id) return null;
|
|
1184
1605
|
const explicit = normalizeWorkflowRoute(config?.agents?.[id])
|
|
@@ -1194,7 +1615,7 @@ function agentRouteFromConfig(config, agentId, dataDir) {
|
|
|
1194
1615
|
if (id === 'explore') return routeFromPreset(config, config?.maintenance?.explore);
|
|
1195
1616
|
if (id === 'maintainer') return routeFromPreset(config, config?.maintenance?.memory);
|
|
1196
1617
|
|
|
1197
|
-
return
|
|
1618
|
+
return null;
|
|
1198
1619
|
}
|
|
1199
1620
|
|
|
1200
1621
|
function toolResponseText(result) {
|
|
@@ -1233,17 +1654,26 @@ function currentSessionRecallRows(session, query, { limit = 10 } = {}) {
|
|
|
1233
1654
|
|
|
1234
1655
|
function parseToolSelection(value) {
|
|
1235
1656
|
if (Array.isArray(value)) return value.map(clean).filter(Boolean);
|
|
1236
|
-
|
|
1657
|
+
if (value && typeof value !== 'string' && typeof value[Symbol.iterator] === 'function') {
|
|
1658
|
+
return [...value].map(clean).filter(Boolean);
|
|
1659
|
+
}
|
|
1660
|
+
return String(value || '').replace(/^select\s*:/i, '')
|
|
1237
1661
|
.split(/[,\s]+/)
|
|
1238
1662
|
.map(clean)
|
|
1239
1663
|
.filter(Boolean);
|
|
1240
1664
|
}
|
|
1241
1665
|
|
|
1666
|
+
function parseToolSearchQuerySelection(query) {
|
|
1667
|
+
const match = clean(query).match(/^select\s*:\s*(.+)$/i);
|
|
1668
|
+
return match ? parseToolSelection(match[1]) : [];
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1242
1671
|
function toolKind(tool) {
|
|
1243
1672
|
const name = clean(tool?.name);
|
|
1244
1673
|
if (name.startsWith('mcp__')) return 'mcp';
|
|
1245
|
-
if (name.startsWith('
|
|
1246
|
-
if (
|
|
1674
|
+
if (name.startsWith('skill:') || tool?.annotations?.mixdogKind === 'skill') return 'skill';
|
|
1675
|
+
if (name === 'Skill' || name.startsWith('skill_') || name === 'skills_list' || name === 'skill_view') return 'skill';
|
|
1676
|
+
if (tool?.annotations?.agentHidden) return 'control';
|
|
1247
1677
|
if (['apply_patch', 'shell'].includes(name)) return 'mutation';
|
|
1248
1678
|
return 'tool';
|
|
1249
1679
|
}
|
|
@@ -1255,9 +1685,9 @@ function toolSchemaBucket(tool) {
|
|
|
1255
1685
|
if (kind === 'skill') return 'skills';
|
|
1256
1686
|
if (name === 'memory' || name === 'recall' || name.includes('memory')) return 'memory';
|
|
1257
1687
|
if (name === 'search' || name === 'web_fetch') return 'web';
|
|
1258
|
-
if (['read', 'grep', 'glob', 'list', 'code_graph', 'explore'].includes(name)) return 'code';
|
|
1688
|
+
if (['read', 'grep', 'find', 'glob', 'list', 'code_graph', 'explore'].includes(name)) return 'code';
|
|
1259
1689
|
if (['shell', 'apply_patch'].includes(name)) return 'mutation';
|
|
1260
|
-
if (name === '
|
|
1690
|
+
if (name === 'agent' || name === 'delegate') return 'agents';
|
|
1261
1691
|
if (name.includes('channel') || name.includes('discord') || name.includes('webhook')) return 'channels';
|
|
1262
1692
|
if (name.includes('provider') || name === 'tool_search' || name === 'cwd') return 'setup';
|
|
1263
1693
|
return 'other';
|
|
@@ -1304,6 +1734,17 @@ function activeToolForSurface(tool) {
|
|
|
1304
1734
|
return JSON.parse(JSON.stringify(tool));
|
|
1305
1735
|
}
|
|
1306
1736
|
|
|
1737
|
+
function deferredProviderMode(provider) {
|
|
1738
|
+
const p = clean(provider).toLowerCase();
|
|
1739
|
+
if (p === 'gemini') return 'full';
|
|
1740
|
+
if (p === 'anthropic' || p === 'anthropic-oauth'
|
|
1741
|
+
|| p === 'openai' || p === 'openai-oauth'
|
|
1742
|
+
|| p === 'xai' || p === 'grok-oauth') {
|
|
1743
|
+
return 'native';
|
|
1744
|
+
}
|
|
1745
|
+
return 'legacy';
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1307
1748
|
function filterDisallowedTools(tools, disallowed = []) {
|
|
1308
1749
|
if (!Array.isArray(disallowed) || disallowed.length === 0) return tools;
|
|
1309
1750
|
const deny = new Set(disallowed.map((name) => clean(name)).filter(Boolean));
|
|
@@ -1324,24 +1765,14 @@ function sortedNamesByMeasuredUsage(names) {
|
|
|
1324
1765
|
}
|
|
1325
1766
|
|
|
1326
1767
|
export function defaultDeferredToolNames(catalog, mode) {
|
|
1768
|
+
const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
1327
1769
|
if (mode === 'lead') {
|
|
1328
|
-
const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
1329
1770
|
return new Set(DEFERRED_DEFAULT_LEAD_TOOLS.filter((name) => available.has(name)));
|
|
1330
1771
|
}
|
|
1331
1772
|
if (mode === 'readonly') {
|
|
1332
|
-
const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
1333
1773
|
return new Set(DEFERRED_DEFAULT_READONLY_TOOLS.filter((name) => available.has(name)));
|
|
1334
1774
|
}
|
|
1335
|
-
|
|
1336
|
-
const limit = DEFERRED_DEFAULT_FULL_LIMIT;
|
|
1337
|
-
for (const tool of sortedCatalogByMeasuredUsage(catalog)) {
|
|
1338
|
-
const name = clean(tool?.name);
|
|
1339
|
-
if (!name || names.has(name)) continue;
|
|
1340
|
-
if (mode === 'readonly' && !isReadonlySelectable(tool)) continue;
|
|
1341
|
-
names.add(name);
|
|
1342
|
-
if (names.size >= DEFERRED_ALWAYS_ACTIVE_TOOLS.size + limit) break;
|
|
1343
|
-
}
|
|
1344
|
-
return names;
|
|
1775
|
+
return new Set(DEFERRED_DEFAULT_FULL_TOOLS.filter((name) => available.has(name)));
|
|
1345
1776
|
}
|
|
1346
1777
|
|
|
1347
1778
|
export function compactToolSearchDescription(value, max = 220) {
|
|
@@ -1360,25 +1791,180 @@ function toolRow(tool, activeNames = new Set()) {
|
|
|
1360
1791
|
};
|
|
1361
1792
|
}
|
|
1362
1793
|
|
|
1794
|
+
function providerSupportsResponsesCustomTools(provider) {
|
|
1795
|
+
const p = clean(provider).toLowerCase();
|
|
1796
|
+
if (!p) return true;
|
|
1797
|
+
return p === 'openai' || p === 'openai-oauth';
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function openAILoadableToolSpec(tool, provider = '') {
|
|
1801
|
+
if (providerSupportsResponsesCustomTools(provider) && isResponsesFreeformTool(tool)) return toResponsesCustomTool(tool);
|
|
1802
|
+
return {
|
|
1803
|
+
type: 'function',
|
|
1804
|
+
name: clean(tool?.name),
|
|
1805
|
+
description: clean(tool?.description),
|
|
1806
|
+
defer_loading: true,
|
|
1807
|
+
parameters: tool?.inputSchema && typeof tool.inputSchema === 'object'
|
|
1808
|
+
? tool.inputSchema
|
|
1809
|
+
: { type: 'object', properties: {} },
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function toolSearchNativePayload(catalog, names, provider = '') {
|
|
1814
|
+
const selected = new Set((names || []).map(clean).filter(Boolean));
|
|
1815
|
+
if (!selected.size) return null;
|
|
1816
|
+
const tools = [];
|
|
1817
|
+
const refs = [];
|
|
1818
|
+
for (const tool of catalog || []) {
|
|
1819
|
+
const name = clean(tool?.name);
|
|
1820
|
+
if (!name || !selected.has(name)) continue;
|
|
1821
|
+
refs.push(name);
|
|
1822
|
+
tools.push(openAILoadableToolSpec(tool, provider));
|
|
1823
|
+
}
|
|
1824
|
+
if (!refs.length) return null;
|
|
1825
|
+
return {
|
|
1826
|
+
toolReferences: refs,
|
|
1827
|
+
openaiTools: tools,
|
|
1828
|
+
summary: `Loaded deferred tools: ${refs.join(', ')}`,
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1363
1832
|
function toolSearchTokens(value) {
|
|
1364
1833
|
return (clean(value).toLowerCase().match(/[a-z0-9_.-]+/g) || [])
|
|
1365
1834
|
.map((token) => token.replace(/[-.]+/g, '_'))
|
|
1366
1835
|
.filter(Boolean);
|
|
1367
1836
|
}
|
|
1368
1837
|
|
|
1838
|
+
function toolSearchMeaningfulTokens(value) {
|
|
1839
|
+
return toolSearchTokens(value).filter((token) => !TOOL_SEARCH_STOP_WORDS.has(token));
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1369
1842
|
function toolSearchText(row) {
|
|
1370
1843
|
const text = `${row.name} ${String(row.name || '').replace(/_/g, ' ')} ${row.kind} ${row.description} ${row.active ? 'active' : 'deferred'}`;
|
|
1371
1844
|
return `${text} ${text.replace(/[-.]+/g, '_')}`.toLowerCase();
|
|
1372
1845
|
}
|
|
1373
1846
|
|
|
1847
|
+
function toolSearchRowAliases(name) {
|
|
1848
|
+
return TOOL_SEARCH_ROW_ALIASES[clean(name)] || [];
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function toolSearchRank(row, query) {
|
|
1852
|
+
const raw = clean(query).toLowerCase();
|
|
1853
|
+
if (!raw) return { score: 0, reasons: [] };
|
|
1854
|
+
const name = clean(row?.name).toLowerCase();
|
|
1855
|
+
const prettyName = name.replace(/_/g, ' ');
|
|
1856
|
+
const haystack = toolSearchText(row);
|
|
1857
|
+
const aliases = toolSearchRowAliases(name);
|
|
1858
|
+
const aliasText = aliases.join(' ').toLowerCase();
|
|
1859
|
+
const queryTokens = toolSearchMeaningfulTokens(raw);
|
|
1860
|
+
const nameTokens = new Set(toolSearchTokens(`${name} ${prettyName}`));
|
|
1861
|
+
const aliasTokens = new Set(toolSearchTokens(aliasText));
|
|
1862
|
+
let score = 0;
|
|
1863
|
+
const reasons = [];
|
|
1864
|
+
if (raw === name || raw === prettyName) {
|
|
1865
|
+
score += 120;
|
|
1866
|
+
reasons.push('exact-name');
|
|
1867
|
+
}
|
|
1868
|
+
if (aliases.some((alias) => raw === alias || raw === alias.replace(/[_-]+/g, ' '))) {
|
|
1869
|
+
score += 100;
|
|
1870
|
+
reasons.push('exact-alias');
|
|
1871
|
+
}
|
|
1872
|
+
if (haystack.includes(raw)) {
|
|
1873
|
+
score += 34;
|
|
1874
|
+
reasons.push('phrase');
|
|
1875
|
+
}
|
|
1876
|
+
for (const alias of aliases) {
|
|
1877
|
+
const normalizedAlias = alias.toLowerCase();
|
|
1878
|
+
if (normalizedAlias && raw.includes(normalizedAlias)) {
|
|
1879
|
+
score += 58;
|
|
1880
|
+
reasons.push('alias-phrase');
|
|
1881
|
+
break;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
let matchedTokens = 0;
|
|
1885
|
+
for (const token of queryTokens) {
|
|
1886
|
+
if (nameTokens.has(token) || name.includes(token)) {
|
|
1887
|
+
score += 24;
|
|
1888
|
+
matchedTokens += 1;
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
if (aliasTokens.has(token) || aliasText.includes(token)) {
|
|
1892
|
+
score += 18;
|
|
1893
|
+
matchedTokens += 1;
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
if (haystack.includes(token)) {
|
|
1897
|
+
score += 7;
|
|
1898
|
+
matchedTokens += 1;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
if (queryTokens.length && matchedTokens === queryTokens.length) {
|
|
1902
|
+
score += Math.min(18, queryTokens.length * 6);
|
|
1903
|
+
reasons.push('all-tokens');
|
|
1904
|
+
}
|
|
1905
|
+
if (clean(row?.kind).toLowerCase() === raw) score += 10;
|
|
1906
|
+
if (score > 0) score += Math.min(6, Math.floor(measuredToolUsage(name) / 150));
|
|
1907
|
+
return { score, reasons };
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1374
1910
|
function toolSearchMatches(row, query) {
|
|
1375
1911
|
const raw = clean(query).toLowerCase();
|
|
1376
1912
|
if (!raw) return true;
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1913
|
+
return toolSearchRank(row, raw).score > 0;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function rankedToolSearchRows(rows, query) {
|
|
1917
|
+
const raw = clean(query).toLowerCase();
|
|
1918
|
+
if (!raw) return rows;
|
|
1919
|
+
return rows
|
|
1920
|
+
.map((row) => {
|
|
1921
|
+
const ranked = toolSearchRank(row, raw);
|
|
1922
|
+
return { ...row, score: ranked.score, reasons: ranked.reasons };
|
|
1923
|
+
})
|
|
1924
|
+
.filter((row) => row.score > 0)
|
|
1925
|
+
.sort((a, b) => {
|
|
1926
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
1927
|
+
if (a.active !== b.active) return a.active ? 1 : -1;
|
|
1928
|
+
if (b.usage !== a.usage) return b.usage - a.usage;
|
|
1929
|
+
return String(a.name).localeCompare(String(b.name));
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
function queryHasAnyPhrase(query, phrases) {
|
|
1934
|
+
const text = clean(query).toLowerCase().replace(/[_-]+/g, ' ');
|
|
1935
|
+
return phrases.some((phrase) => text.includes(phrase));
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
function autoToolSelectionNames(query, rows) {
|
|
1939
|
+
const raw = clean(query).toLowerCase();
|
|
1940
|
+
if (!raw || TOOL_SEARCH_AMBIGUOUS_AUTO_QUERIES.has(raw)) return [];
|
|
1941
|
+
if (TOOL_SEARCH_SAFE_AUTO_ALIASES.has(raw) && DEFERRED_SELECT_ALIASES[raw]) {
|
|
1942
|
+
return DEFERRED_SELECT_ALIASES[raw];
|
|
1943
|
+
}
|
|
1944
|
+
if (queryHasAnyPhrase(raw, ['save memory', 'store memory', 'delete memory', 'forget memory', 'memory status', '기억 저장', '메모리 저장'])) {
|
|
1945
|
+
return ['memory'];
|
|
1946
|
+
}
|
|
1947
|
+
if (queryHasAnyPhrase(raw, ['run', 'execute', 'test', 'tests', 'build', 'terminal', 'command', 'powershell', 'bash', 'shell', 'npm', 'node', 'git', '실행', '테스트', '빌드', '터미널', '쉘'])) {
|
|
1948
|
+
return ['shell', 'task'];
|
|
1949
|
+
}
|
|
1950
|
+
if (queryHasAnyPhrase(raw, ['web', 'internet', 'online', 'current info', 'latest', 'news', 'browse', 'docs', 'documentation', '웹', '인터넷', '최신', '뉴스', '문서'])) {
|
|
1951
|
+
return ['search', 'web_fetch'];
|
|
1952
|
+
}
|
|
1953
|
+
if (queryHasAnyPhrase(raw, ['recall', 'remember', 'memory previous', 'previous', 'history', 'past', 'prior', 'earlier', 'resume', '이전', '기억', '히스토리'])) {
|
|
1954
|
+
return ['recall'];
|
|
1955
|
+
}
|
|
1956
|
+
if (queryHasAnyPhrase(raw, ['delegate', 'subagent', 'worker', 'parallel agent', 'background agent', 'reviewer', 'explorer', '에이전트', '워커', '병렬'])) {
|
|
1957
|
+
return ['agent'];
|
|
1958
|
+
}
|
|
1959
|
+
if (queryHasAnyPhrase(raw, ['working directory', 'project root', 'current directory', 'cwd'])) {
|
|
1960
|
+
return ['cwd'];
|
|
1961
|
+
}
|
|
1962
|
+
const ranked = rankedToolSearchRows(rows, raw);
|
|
1963
|
+
const top = ranked[0];
|
|
1964
|
+
if (!top) return [];
|
|
1965
|
+
const reasons = new Set(top.reasons || []);
|
|
1966
|
+
if (reasons.has('exact-name') || reasons.has('exact-alias')) return [top.name];
|
|
1967
|
+
return [];
|
|
1382
1968
|
}
|
|
1383
1969
|
|
|
1384
1970
|
function expandSelectionNames(names) {
|
|
@@ -1393,6 +1979,39 @@ function expandSelectionNames(names) {
|
|
|
1393
1979
|
return [...new Set(out)];
|
|
1394
1980
|
}
|
|
1395
1981
|
|
|
1982
|
+
function storedDeferredToolNames(session) {
|
|
1983
|
+
for (const source of [session?.deferredDiscoveredTools, session?.deferredSelectedTools]) {
|
|
1984
|
+
const names = parseToolSelection(source);
|
|
1985
|
+
if (names.length) return names;
|
|
1986
|
+
}
|
|
1987
|
+
return [];
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
function canonicalDeferredToolNames(catalog, names) {
|
|
1991
|
+
const byName = new Map();
|
|
1992
|
+
for (const tool of catalog || []) {
|
|
1993
|
+
const name = clean(tool?.name);
|
|
1994
|
+
if (!name) continue;
|
|
1995
|
+
byName.set(name, name);
|
|
1996
|
+
byName.set(name.toLowerCase(), name);
|
|
1997
|
+
}
|
|
1998
|
+
const out = [];
|
|
1999
|
+
for (const raw of expandSelectionNames(names)) {
|
|
2000
|
+
const name = clean(raw);
|
|
2001
|
+
const canonical = byName.get(name) || byName.get(name.toLowerCase());
|
|
2002
|
+
if (canonical) out.push(canonical);
|
|
2003
|
+
}
|
|
2004
|
+
return sortedNamesByMeasuredUsage(new Set(out));
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
function setDeferredToolState(session, names) {
|
|
2008
|
+
if (!session) return [];
|
|
2009
|
+
const selected = sortedNamesByMeasuredUsage(new Set(parseToolSelection(names)));
|
|
2010
|
+
session.deferredDiscoveredTools = selected;
|
|
2011
|
+
session.deferredSelectedTools = selected;
|
|
2012
|
+
return selected;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
1396
2015
|
function isReadonlySelectable(tool) {
|
|
1397
2016
|
const name = clean(tool?.name);
|
|
1398
2017
|
if (READONLY_TOOL_NAMES.has(name)) return true;
|
|
@@ -1402,8 +2021,9 @@ function isReadonlySelectable(tool) {
|
|
|
1402
2021
|
return false;
|
|
1403
2022
|
}
|
|
1404
2023
|
|
|
1405
|
-
function applyDeferredToolSurface(session, mode, extraTools = []) {
|
|
2024
|
+
function applyDeferredToolSurface(session, mode, extraTools = [], options = {}) {
|
|
1406
2025
|
if (!session || !Array.isArray(session.tools)) return session;
|
|
2026
|
+
const providerMode = deferredProviderMode(options.provider || session.provider);
|
|
1407
2027
|
const byName = new Map();
|
|
1408
2028
|
for (const tool of [...session.tools, ...(extraTools || [])]) {
|
|
1409
2029
|
const name = clean(tool?.name);
|
|
@@ -1412,14 +2032,34 @@ function applyDeferredToolSurface(session, mode, extraTools = []) {
|
|
|
1412
2032
|
}
|
|
1413
2033
|
const catalog = sortedCatalogByMeasuredUsage([...byName.values()]);
|
|
1414
2034
|
const defaultNames = defaultDeferredToolNames(catalog, mode);
|
|
2035
|
+
const storedNames = providerMode === 'native' ? [] : storedDeferredToolNames(session);
|
|
2036
|
+
let selectedNames = providerMode === 'full'
|
|
2037
|
+
? sortedNamesByMeasuredUsage(catalog.map((tool) => clean(tool?.name)).filter(Boolean))
|
|
2038
|
+
: [];
|
|
2039
|
+
if (providerMode !== 'full') {
|
|
2040
|
+
selectedNames = storedNames.length ? canonicalDeferredToolNames(catalog, storedNames) : [];
|
|
2041
|
+
if (!selectedNames.length || providerMode === 'native') selectedNames = sortedNamesByMeasuredUsage(defaultNames);
|
|
2042
|
+
}
|
|
2043
|
+
const selected = new Set(selectedNames);
|
|
1415
2044
|
session.deferredToolCatalog = catalog;
|
|
1416
2045
|
session.deferredToolUsage = MEASURED_TOOL_USAGE;
|
|
1417
|
-
session.
|
|
2046
|
+
session.deferredDefaultTools = sortedNamesByMeasuredUsage(defaultNames);
|
|
2047
|
+
session.deferredProviderMode = providerMode;
|
|
2048
|
+
session.deferredNativeTools = providerMode === 'native';
|
|
1418
2049
|
session.tools.length = 0;
|
|
2050
|
+
const active = [];
|
|
1419
2051
|
for (const tool of catalog) {
|
|
1420
|
-
if (!
|
|
2052
|
+
if (!selected.has(clean(tool?.name))) continue;
|
|
1421
2053
|
if (mode === 'readonly' && !isReadonlySelectable(tool)) continue;
|
|
1422
2054
|
session.tools.push(tool);
|
|
2055
|
+
active.push(clean(tool?.name));
|
|
2056
|
+
}
|
|
2057
|
+
if (providerMode === 'native') {
|
|
2058
|
+
const discovered = canonicalDeferredToolNames(catalog, session.deferredDiscoveredTools || []);
|
|
2059
|
+
session.deferredSelectedTools = active;
|
|
2060
|
+
session.deferredDiscoveredTools = discovered.filter((name) => !selected.has(name));
|
|
2061
|
+
} else {
|
|
2062
|
+
setDeferredToolState(session, active);
|
|
1423
2063
|
}
|
|
1424
2064
|
return session;
|
|
1425
2065
|
}
|
|
@@ -1428,57 +2068,106 @@ function selectDeferredTools(session, names, mode) {
|
|
|
1428
2068
|
const catalog = Array.isArray(session?.deferredToolCatalog)
|
|
1429
2069
|
? session.deferredToolCatalog
|
|
1430
2070
|
: (Array.isArray(session?.tools) ? session.tools : []);
|
|
1431
|
-
const active = new Set((session?.tools || []).map((tool) => tool?.name).filter(Boolean));
|
|
1432
|
-
const
|
|
2071
|
+
const active = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
2072
|
+
const native = session?.deferredProviderMode === 'native' || session?.deferredNativeTools === true;
|
|
2073
|
+
const discovered = new Set(Array.isArray(session?.deferredDiscoveredTools) ? session.deferredDiscoveredTools : []);
|
|
2074
|
+
const byName = new Map();
|
|
2075
|
+
for (const tool of catalog) {
|
|
2076
|
+
const name = clean(tool?.name);
|
|
2077
|
+
if (!name) continue;
|
|
2078
|
+
byName.set(name, tool);
|
|
2079
|
+
byName.set(name.toLowerCase(), tool);
|
|
2080
|
+
}
|
|
1433
2081
|
const added = [];
|
|
1434
2082
|
const already = [];
|
|
1435
2083
|
const blocked = [];
|
|
1436
2084
|
const missing = [];
|
|
1437
|
-
for (const
|
|
1438
|
-
const
|
|
2085
|
+
for (const rawName of expandSelectionNames(names)) {
|
|
2086
|
+
const requestedName = clean(rawName);
|
|
2087
|
+
const tool = byName.get(requestedName) || byName.get(requestedName.toLowerCase());
|
|
2088
|
+
const name = clean(tool?.name);
|
|
1439
2089
|
if (!tool) {
|
|
1440
|
-
missing.push(
|
|
2090
|
+
missing.push(requestedName);
|
|
1441
2091
|
continue;
|
|
1442
2092
|
}
|
|
1443
2093
|
if (mode === 'readonly' && !isReadonlySelectable(tool)) {
|
|
1444
2094
|
blocked.push({ name, reason: 'readonly mode' });
|
|
1445
2095
|
continue;
|
|
1446
2096
|
}
|
|
1447
|
-
if (active.has(name)) {
|
|
2097
|
+
if (active.has(name) || discovered.has(name)) {
|
|
1448
2098
|
already.push(name);
|
|
1449
2099
|
continue;
|
|
1450
2100
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
2101
|
+
if (native) {
|
|
2102
|
+
discovered.add(name);
|
|
2103
|
+
} else {
|
|
2104
|
+
session.tools.push(tool);
|
|
2105
|
+
active.add(name);
|
|
2106
|
+
}
|
|
1453
2107
|
added.push(name);
|
|
1454
2108
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
2109
|
+
if (native) {
|
|
2110
|
+
session.deferredDiscoveredTools = sortedNamesByMeasuredUsage(discovered);
|
|
2111
|
+
session.deferredSelectedTools = sortedNamesByMeasuredUsage(active);
|
|
2112
|
+
} else {
|
|
2113
|
+
setDeferredToolState(session, active);
|
|
2114
|
+
}
|
|
2115
|
+
return { added, already, blocked, missing, native };
|
|
1457
2116
|
}
|
|
1458
2117
|
|
|
1459
2118
|
function renderToolSearch(args = {}, session, mode = 'full') {
|
|
1460
2119
|
const catalog = Array.isArray(session?.deferredToolCatalog)
|
|
1461
2120
|
? session.deferredToolCatalog
|
|
1462
2121
|
: (Array.isArray(session?.tools) ? session.tools : []);
|
|
1463
|
-
const
|
|
1464
|
-
const
|
|
1465
|
-
const
|
|
2122
|
+
const rawQuery = clean(args.query);
|
|
2123
|
+
const explicitSelectedNames = parseToolSelection(args.select);
|
|
2124
|
+
const querySelectedNames = explicitSelectedNames.length ? [] : parseToolSearchQuerySelection(rawQuery);
|
|
2125
|
+
const forcedSelectedNames = explicitSelectedNames.length ? explicitSelectedNames : querySelectedNames;
|
|
2126
|
+
const query = querySelectedNames.length ? '' : rawQuery.toLowerCase();
|
|
1466
2127
|
const limit = Math.max(1, Math.min(50, Number(args.limit) || 20));
|
|
1467
|
-
const
|
|
1468
|
-
const
|
|
1469
|
-
const
|
|
2128
|
+
const initialActiveNames = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
2129
|
+
const initialRows = catalog.map((tool) => toolRow(tool, initialActiveNames)).filter((row) => row.name);
|
|
2130
|
+
const autoSelectedNames = (!forcedSelectedNames.length && query)
|
|
2131
|
+
? autoToolSelectionNames(query, initialRows)
|
|
2132
|
+
: [];
|
|
2133
|
+
const selectedNames = forcedSelectedNames.length ? forcedSelectedNames : autoSelectedNames;
|
|
2134
|
+
const toolSelection = selectedNames.length ? selectDeferredTools(session, selectedNames, mode) : null;
|
|
2135
|
+
const selectionMode = forcedSelectedNames.length ? 'select' : (autoSelectedNames.length ? 'auto' : null);
|
|
2136
|
+
const nextActiveNames = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
2137
|
+
const rows = [
|
|
2138
|
+
...catalog.map((tool) => toolRow(tool, nextActiveNames)),
|
|
2139
|
+
].filter((row) => row.name);
|
|
1470
2140
|
const matches = query
|
|
1471
|
-
? rows
|
|
2141
|
+
? rankedToolSearchRows(rows, query)
|
|
1472
2142
|
: rows;
|
|
2143
|
+
const selected = toolSelection
|
|
2144
|
+
? {
|
|
2145
|
+
mode: selectionMode,
|
|
2146
|
+
tools: toolSelection,
|
|
2147
|
+
}
|
|
2148
|
+
: null;
|
|
2149
|
+
const nativeToolSearch = toolSelection?.native
|
|
2150
|
+
? toolSearchNativePayload(catalog, toolSelection.added, session?.provider)
|
|
2151
|
+
: null;
|
|
1473
2152
|
return JSON.stringify({
|
|
1474
|
-
selected
|
|
2153
|
+
selected,
|
|
2154
|
+
...(nativeToolSearch ? { nativeToolSearch } : {}),
|
|
1475
2155
|
totalMatches: matches.length,
|
|
1476
2156
|
matches: matches.slice(0, limit),
|
|
1477
2157
|
activeTools: sortedNamesByMeasuredUsage(nextActiveNames),
|
|
1478
|
-
|
|
2158
|
+
discoveredTools: sortedNamesByMeasuredUsage(session?.deferredDiscoveredTools || []),
|
|
2159
|
+
note: 'query ranks matches and auto-loads high-confidence deferred tools; select exact tool names, or query select:a,b, to force load.',
|
|
1479
2160
|
}, null, 2);
|
|
1480
2161
|
}
|
|
1481
2162
|
|
|
2163
|
+
export function __renderToolSearchForTest(args = {}, session = {}, mode = 'full') {
|
|
2164
|
+
return renderToolSearch(args, session, mode);
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
export function __saveModelSettingsForTest(cfgMod, route, options = {}) {
|
|
2168
|
+
return saveModelSettings(cfgMod, route, options);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
1482
2171
|
function resolveCwdPath(currentCwd, value) {
|
|
1483
2172
|
const raw = clean(value);
|
|
1484
2173
|
if (!raw) throw new Error('cwd: path is required for action=set');
|
|
@@ -1501,7 +2190,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1501
2190
|
rootDir: STANDALONE_ROOT,
|
|
1502
2191
|
dataDir: STANDALONE_DATA_DIR,
|
|
1503
2192
|
});
|
|
1504
|
-
ensureProjectMixdogMd({ cwd });
|
|
1505
2193
|
bootProfile('standalone-env:ready', { ms: (performance.now() - standaloneStartedAt).toFixed(1) });
|
|
1506
2194
|
|
|
1507
2195
|
const importsStartedAt = performance.now();
|
|
@@ -1533,18 +2221,51 @@ export async function createMixdogSessionRuntime({
|
|
|
1533
2221
|
profiledImport('code-graph-tool-defs', CODE_GRAPH_TOOL_DEFS, { optional: true }),
|
|
1534
2222
|
]);
|
|
1535
2223
|
bootProfile('imports:ready', { ms: (performance.now() - importsStartedAt).toFixed(1) });
|
|
2224
|
+
const pluginDataDir = cfgMod.getPluginData();
|
|
2225
|
+
const memoryRuntime = createStandaloneMemoryRuntime({
|
|
2226
|
+
entry: join(STANDALONE_ROOT, MEMORY_RUNTIME.replace(/^\.\//, '')),
|
|
2227
|
+
dataDir: pluginDataDir,
|
|
2228
|
+
cwd,
|
|
2229
|
+
});
|
|
1536
2230
|
let memoryModPromise = null;
|
|
1537
|
-
let memoryInitPromise = null;
|
|
1538
2231
|
let searchModPromise = null;
|
|
1539
2232
|
let codeGraphModPromise = null;
|
|
2233
|
+
let outputStyleStatusCache = null;
|
|
2234
|
+
let outputStyleStatusCacheAt = 0;
|
|
2235
|
+
let outputStyleStatusCacheDir = '';
|
|
2236
|
+
|
|
2237
|
+
const memoryEnabled = () => moduleEnabled(config, 'memory', true);
|
|
2238
|
+
const channelsEnabled = () => moduleEnabled(config, 'channels', true);
|
|
2239
|
+
const getOutputStyleStatusCached = ({ fresh = false } = {}) => {
|
|
2240
|
+
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
2241
|
+
const cacheDir = resolve(dataDir);
|
|
2242
|
+
const now = performance.now();
|
|
2243
|
+
if (
|
|
2244
|
+
!fresh
|
|
2245
|
+
&& outputStyleStatusCache
|
|
2246
|
+
&& outputStyleStatusCacheDir === cacheDir
|
|
2247
|
+
&& now - outputStyleStatusCacheAt < 2500
|
|
2248
|
+
) {
|
|
2249
|
+
return outputStyleStatusCache;
|
|
2250
|
+
}
|
|
2251
|
+
outputStyleStatusCache = outputStyleStatus(dataDir);
|
|
2252
|
+
outputStyleStatusCacheAt = now;
|
|
2253
|
+
outputStyleStatusCacheDir = cacheDir;
|
|
2254
|
+
return outputStyleStatusCache;
|
|
2255
|
+
};
|
|
2256
|
+
const invalidateOutputStyleStatusCache = () => {
|
|
2257
|
+
outputStyleStatusCache = null;
|
|
2258
|
+
outputStyleStatusCacheAt = 0;
|
|
2259
|
+
outputStyleStatusCacheDir = '';
|
|
2260
|
+
};
|
|
1540
2261
|
|
|
1541
2262
|
async function getMemoryModule() {
|
|
2263
|
+
if (!memoryEnabled()) throw new Error('memory is disabled in settings');
|
|
1542
2264
|
const startedAt = performance.now();
|
|
1543
|
-
memoryModPromise ??=
|
|
2265
|
+
memoryModPromise ??= Promise.resolve(memoryRuntime);
|
|
1544
2266
|
const mod = await memoryModPromise;
|
|
1545
2267
|
if (typeof mod?.init === 'function') {
|
|
1546
|
-
|
|
1547
|
-
await memoryInitPromise;
|
|
2268
|
+
await mod.init();
|
|
1548
2269
|
}
|
|
1549
2270
|
bootProfile('memory-runtime:ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
1550
2271
|
return mod;
|
|
@@ -1558,6 +2279,211 @@ export async function createMixdogSessionRuntime({
|
|
|
1558
2279
|
return mod;
|
|
1559
2280
|
}
|
|
1560
2281
|
|
|
2282
|
+
function normalizeSearchAllowedDomain(site) {
|
|
2283
|
+
const raw = clean(site);
|
|
2284
|
+
if (!raw) return '';
|
|
2285
|
+
try {
|
|
2286
|
+
return new URL(/^https?:\/\//i.test(raw) ? raw : `https://${raw}`).hostname.toLowerCase();
|
|
2287
|
+
} catch {
|
|
2288
|
+
return raw.replace(/^https?:\/\//i, '').split('/')[0].toLowerCase();
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function nativeSearchUserLocation(locale) {
|
|
2293
|
+
if (!locale || typeof locale !== 'object' || Array.isArray(locale)) return null;
|
|
2294
|
+
const location = { type: 'approximate' };
|
|
2295
|
+
for (const key of ['country', 'region', 'city', 'timezone']) {
|
|
2296
|
+
const value = clean(locale[key]);
|
|
2297
|
+
if (value) location[key] = value;
|
|
2298
|
+
}
|
|
2299
|
+
return Object.keys(location).length > 1 ? location : null;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
function nativeSearchTool(args = {}, toolType = 'web_search', providerId = '') {
|
|
2303
|
+
const providerName = normalizeSearchProviderId(providerId);
|
|
2304
|
+
const domain = normalizeSearchAllowedDomain(args.site);
|
|
2305
|
+
const type = clean(toolType) || 'web_search';
|
|
2306
|
+
const location = nativeSearchUserLocation(args.locale);
|
|
2307
|
+
if (providerName === 'gemini') {
|
|
2308
|
+
return { type: type || 'google_search' };
|
|
2309
|
+
}
|
|
2310
|
+
if (providerName === 'anthropic' || providerName === 'anthropic-oauth') {
|
|
2311
|
+
const tool = {
|
|
2312
|
+
type: 'web_search_20250305',
|
|
2313
|
+
name: 'web_search',
|
|
2314
|
+
max_uses: Math.max(1, Math.min(10, Number(args.maxResults) || 5)),
|
|
2315
|
+
};
|
|
2316
|
+
if (domain) tool.allowed_domains = [domain];
|
|
2317
|
+
if (location) tool.user_location = location;
|
|
2318
|
+
return tool;
|
|
2319
|
+
}
|
|
2320
|
+
if (providerName === 'grok-oauth' || providerName === 'xai') {
|
|
2321
|
+
const tool = { type };
|
|
2322
|
+
if (domain) tool.filters = { allowed_domains: [domain] };
|
|
2323
|
+
return tool;
|
|
2324
|
+
}
|
|
2325
|
+
const tool = {
|
|
2326
|
+
type,
|
|
2327
|
+
};
|
|
2328
|
+
if (type === 'web_search') {
|
|
2329
|
+
tool.search_context_size = clean(args.contextSize) || 'low';
|
|
2330
|
+
if (domain) tool.filters = { allowed_domains: [domain] };
|
|
2331
|
+
if (location) tool.user_location = location;
|
|
2332
|
+
}
|
|
2333
|
+
return tool;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
function nativeSearchToolTypes(routeLike = {}) {
|
|
2337
|
+
const envToolType = clean(process.env.MIXDOG_NATIVE_SEARCH_TOOL_TYPE);
|
|
2338
|
+
if (envToolType) return [envToolType];
|
|
2339
|
+
const configured = clean(routeLike.toolType);
|
|
2340
|
+
if (configured) return [configured];
|
|
2341
|
+
const providerName = normalizeSearchProviderId(routeLike.provider);
|
|
2342
|
+
if (providerName === 'gemini') return ['google_search'];
|
|
2343
|
+
if (providerName === 'anthropic' || providerName === 'anthropic-oauth') return ['web_search'];
|
|
2344
|
+
if (providerName === 'grok-oauth' || providerName === 'xai') return ['web_search'];
|
|
2345
|
+
return ['web_search', 'web_search_preview'];
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
function currentMainSearchModelMeta() {
|
|
2349
|
+
if (!route?.provider || !route?.model) return null;
|
|
2350
|
+
return { ...route, id: route.model, display: route.model, name: route.model };
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
function nativeSearchRoutes() {
|
|
2354
|
+
const cfg = ensureFullConfig();
|
|
2355
|
+
searchRoute = normalizeSearchRouteConfig(cfg.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
2356
|
+
if (!searchRoute) return [];
|
|
2357
|
+
if (isDefaultSearchRouteConfig(searchRoute)) {
|
|
2358
|
+
const mainModel = currentMainSearchModelMeta();
|
|
2359
|
+
if (!mainModel || !searchCapableFor(route.provider, mainModel)) return [];
|
|
2360
|
+
return [{
|
|
2361
|
+
key: `default\n${route.provider}\n${route.model}`,
|
|
2362
|
+
provider: normalizeSearchProviderId(route.provider),
|
|
2363
|
+
model: route.model,
|
|
2364
|
+
source: 'default-search-route',
|
|
2365
|
+
effort: route.effectiveEffort || route.effort || null,
|
|
2366
|
+
fast: route.fast === true,
|
|
2367
|
+
toolType: searchRoute.toolType || null,
|
|
2368
|
+
}];
|
|
2369
|
+
}
|
|
2370
|
+
const providerName = normalizeSearchProviderId(searchRoute.provider);
|
|
2371
|
+
if (!isSearchCapableProvider(providerName)) return [];
|
|
2372
|
+
return [{
|
|
2373
|
+
key: `${providerName}\n${searchRoute.model}`,
|
|
2374
|
+
provider: providerName,
|
|
2375
|
+
model: searchRoute.model,
|
|
2376
|
+
source: 'search-route',
|
|
2377
|
+
effort: searchRoute.effort || null,
|
|
2378
|
+
fast: searchRoute.fast === true,
|
|
2379
|
+
toolType: searchRoute.toolType || null,
|
|
2380
|
+
}];
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function nativeSearchMessages(searchArgs = {}) {
|
|
2384
|
+
const prompt = searchArgs.prompt || '';
|
|
2385
|
+
return [
|
|
2386
|
+
{
|
|
2387
|
+
role: 'system',
|
|
2388
|
+
content: [
|
|
2389
|
+
'You are Mixdog native web search.',
|
|
2390
|
+
'Use the hosted web_search tool for current or external facts.',
|
|
2391
|
+
'Answer concisely, cite source URLs, and do not request local tools or file edits.',
|
|
2392
|
+
].join('\n'),
|
|
2393
|
+
},
|
|
2394
|
+
{ role: 'user', content: prompt },
|
|
2395
|
+
];
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function flattenNativeSearchSources(result = {}) {
|
|
2399
|
+
const out = [];
|
|
2400
|
+
const add = (source, fallbackTitle = '') => {
|
|
2401
|
+
if (!source || typeof source !== 'object') return;
|
|
2402
|
+
const url = clean(source.url || source.uri || source.href || source.source_url);
|
|
2403
|
+
if (!url) return;
|
|
2404
|
+
out.push({
|
|
2405
|
+
title: clean(source.title || source.query || source.name || fallbackTitle || url),
|
|
2406
|
+
url,
|
|
2407
|
+
snippet: clean(source.snippet || source.text || source.description),
|
|
2408
|
+
source: source.source || 'native-web-search',
|
|
2409
|
+
provider: source.provider || 'native-web-search',
|
|
2410
|
+
});
|
|
2411
|
+
};
|
|
2412
|
+
for (const citation of Array.isArray(result.citations) ? result.citations : []) add(citation);
|
|
2413
|
+
for (const call of Array.isArray(result.webSearchCalls) ? result.webSearchCalls : []) {
|
|
2414
|
+
const action = call?.action || {};
|
|
2415
|
+
for (const source of Array.isArray(action.sources) ? action.sources : []) add(source, action.query || '');
|
|
2416
|
+
if (action.url) add({ url: action.url, title: action.query || '' });
|
|
2417
|
+
for (const url of Array.isArray(action.urls) ? action.urls : []) add({ url, title: action.query || '' });
|
|
2418
|
+
}
|
|
2419
|
+
const seen = new Set();
|
|
2420
|
+
return out.filter((item) => {
|
|
2421
|
+
const key = item.url || `${item.title}\n${item.snippet}`;
|
|
2422
|
+
if (!key || seen.has(key)) return false;
|
|
2423
|
+
seen.add(key);
|
|
2424
|
+
return true;
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
async function runNativeWebSearch(searchArgs = {}, { signal } = {}) {
|
|
2429
|
+
const candidates = nativeSearchRoutes();
|
|
2430
|
+
if (!candidates.length) {
|
|
2431
|
+
if (isDefaultSearchRouteConfig(searchRoute)) {
|
|
2432
|
+
throw new Error(`default search route requires the current main model to support native web search (${route?.provider || 'unknown'}/${route?.model || 'unknown'})`);
|
|
2433
|
+
}
|
|
2434
|
+
throw new Error('search route is not configured; open /search to choose a search provider/model');
|
|
2435
|
+
}
|
|
2436
|
+
const errors = [];
|
|
2437
|
+
for (const candidate of candidates) {
|
|
2438
|
+
for (const toolType of nativeSearchToolTypes(candidate)) {
|
|
2439
|
+
try {
|
|
2440
|
+
await ensureProvidersReady(ensureProviderEnabled(config, candidate.provider));
|
|
2441
|
+
const providerImpl = reg.getProvider(candidate.provider);
|
|
2442
|
+
if (!providerImpl || typeof providerImpl.send !== 'function') {
|
|
2443
|
+
throw new Error(`provider "${candidate.provider}" is not ready`);
|
|
2444
|
+
}
|
|
2445
|
+
const model = candidate.model;
|
|
2446
|
+
const searchTool = nativeSearchTool(searchArgs, toolType, candidate.provider);
|
|
2447
|
+
const startedAt = Date.now();
|
|
2448
|
+
const result = await providerImpl.send(
|
|
2449
|
+
nativeSearchMessages(searchArgs),
|
|
2450
|
+
model,
|
|
2451
|
+
undefined,
|
|
2452
|
+
{
|
|
2453
|
+
signal,
|
|
2454
|
+
role: 'web-search',
|
|
2455
|
+
sessionId: `${session?.id || 'search'}:native-search:${Date.now().toString(36)}`,
|
|
2456
|
+
sourceType: 'native-search',
|
|
2457
|
+
sourceName: 'search',
|
|
2458
|
+
nativeTools: [searchTool],
|
|
2459
|
+
nativeInclude: candidate.provider === 'openai' || candidate.provider === 'openai-oauth'
|
|
2460
|
+
? ['web_search_call.action.sources']
|
|
2461
|
+
: [],
|
|
2462
|
+
toolChoice: candidate.provider === 'gemini' ? 'auto' : 'required',
|
|
2463
|
+
...(candidate.effort ? { effort: candidate.effort } : {}),
|
|
2464
|
+
fast: candidate.fast === true,
|
|
2465
|
+
onStageChange: () => {},
|
|
2466
|
+
onStreamDelta: () => {},
|
|
2467
|
+
},
|
|
2468
|
+
);
|
|
2469
|
+
const sources = flattenNativeSearchSources(result);
|
|
2470
|
+
return {
|
|
2471
|
+
content: String(result?.content || '').trim(),
|
|
2472
|
+
provider: candidate.provider,
|
|
2473
|
+
model: result?.model || candidate.model || null,
|
|
2474
|
+
usage: result?.usage || null,
|
|
2475
|
+
citations: sources,
|
|
2476
|
+
webSearchCalls: result?.webSearchCalls || [],
|
|
2477
|
+
durationMs: Date.now() - startedAt,
|
|
2478
|
+
};
|
|
2479
|
+
} catch (err) {
|
|
2480
|
+
errors.push(`${candidate.provider}${candidate.model ? `/${candidate.model}` : ''}/${toolType}: ${err?.message || String(err)}`);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
throw new Error(`native web search failed: ${errors.join(' | ')}`);
|
|
2485
|
+
}
|
|
2486
|
+
|
|
1561
2487
|
async function getCodeGraphModule() {
|
|
1562
2488
|
const startedAt = performance.now();
|
|
1563
2489
|
codeGraphModPromise ??= import(CODE_GRAPH_RUNTIME);
|
|
@@ -1578,8 +2504,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1578
2504
|
};
|
|
1579
2505
|
nextConfig.default = workflowPresetId('lead');
|
|
1580
2506
|
|
|
1581
|
-
|
|
1582
|
-
config = nextConfig;
|
|
2507
|
+
saveConfigAndAdopt(nextConfig);
|
|
1583
2508
|
return leadRoute;
|
|
1584
2509
|
}
|
|
1585
2510
|
|
|
@@ -1620,6 +2545,10 @@ export async function createMixdogSessionRuntime({
|
|
|
1620
2545
|
async function loadCoreMemoryContext() {
|
|
1621
2546
|
// Boot should not pay for memory/PG startup unless explicitly requested.
|
|
1622
2547
|
// Recall and memory tools still initialize the memory service on first use.
|
|
2548
|
+
if (!memoryEnabled()) {
|
|
2549
|
+
bootProfile('core-memory:disabled');
|
|
2550
|
+
return '';
|
|
2551
|
+
}
|
|
1623
2552
|
if (process.env.MIXDOG_BOOT_CORE_MEMORY !== '1') {
|
|
1624
2553
|
bootProfile('core-memory:skipped');
|
|
1625
2554
|
return '';
|
|
@@ -1652,32 +2581,100 @@ export async function createMixdogSessionRuntime({
|
|
|
1652
2581
|
setConfiguredShell(normalizeSystemShellConfig(config.shell).command);
|
|
1653
2582
|
let configHasSecrets = false;
|
|
1654
2583
|
let route = resolveRoute(config, { provider, model });
|
|
2584
|
+
let searchRoute = normalizeSearchRouteConfig(config.searchRoute);
|
|
1655
2585
|
bootProfile('config:ready', { ms: (performance.now() - configStartedAt).toFixed(1) });
|
|
1656
2586
|
let mode = normalizeToolMode(toolMode);
|
|
1657
2587
|
let session = null;
|
|
1658
2588
|
let sessionCreatePromise = null;
|
|
1659
2589
|
let currentCwd = cwd;
|
|
1660
2590
|
let sessionNeedsCwdRefresh = false;
|
|
1661
|
-
const workspaceRouter = createWorkspaceRouter({ entryCwd: cwd });
|
|
1662
2591
|
let closeRequested = false;
|
|
1663
2592
|
let channelStartTimer = null;
|
|
2593
|
+
let providerSetupWarmupTimer = null;
|
|
1664
2594
|
let providerWarmupTimer = null;
|
|
1665
2595
|
let providerModelWarmupTimer = null;
|
|
2596
|
+
let codeGraphPrewarmTimer = null;
|
|
2597
|
+
let codeGraphPrewarmInFlight = false;
|
|
2598
|
+
let codeGraphPrewarmQueuedCwd = '';
|
|
2599
|
+
let statuslineUsageWarmupTimer = null;
|
|
2600
|
+
let statuslineUsageRefreshTimer = null;
|
|
1666
2601
|
let activeTurnCount = 0;
|
|
1667
2602
|
let firstTurnCompleted = false;
|
|
2603
|
+
function hookTranscriptPath(sessionId) {
|
|
2604
|
+
const id = clean(sessionId);
|
|
2605
|
+
if (!id || !/^[A-Za-z0-9_-]+$/.test(id)) return null;
|
|
2606
|
+
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
2607
|
+
return join(dataDir, 'sessions', `${id}.json`);
|
|
2608
|
+
}
|
|
2609
|
+
function hookEffortPayload() {
|
|
2610
|
+
const level = clean(route.effectiveEffort || route.effort);
|
|
2611
|
+
return level ? { level: level.toLowerCase() } : undefined;
|
|
2612
|
+
}
|
|
2613
|
+
function hookCommonPayload(extra = {}) {
|
|
2614
|
+
const sid = clean(extra.session_id || extra.sessionId || session?.id);
|
|
2615
|
+
return {
|
|
2616
|
+
...(sid ? { session_id: sid, transcript_path: hookTranscriptPath(sid) } : {}),
|
|
2617
|
+
cwd: currentCwd,
|
|
2618
|
+
permission_mode: session?.permissionMode || 'default',
|
|
2619
|
+
...(hookEffortPayload() ? { effort: hookEffortPayload() } : {}),
|
|
2620
|
+
...extra,
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
1668
2623
|
const sessionPrewarmDelayMs = envDelayMs('MIXDOG_SESSION_PREWARM_DELAY_MS', 50, { min: 0, max: 10_000 });
|
|
2624
|
+
const providerSetupWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_SETUP_WARMUP_DELAY_MS', 300, { min: 0, max: 60_000 });
|
|
1669
2625
|
const providerWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_WARMUP_DELAY_MS', 1_500, { min: 0, max: 60_000 });
|
|
1670
|
-
|
|
2626
|
+
// Background model-catalog prefetch delay. Kept short so the first `/model`
|
|
2627
|
+
// open finds a warm cache instead of paying a cold full network load. The
|
|
2628
|
+
// work is async + unref'd, so short-lived detached runtimes still exit
|
|
2629
|
+
// cleanly without waiting on it. Operators can raise it via env if a
|
|
2630
|
+
// detached runtime must avoid the /models round-trip entirely.
|
|
2631
|
+
const providerModelWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS', 2_000, { min: 0, max: 120_000 });
|
|
2632
|
+
const codeGraphPrewarmDelayMs = envDelayMs('MIXDOG_CODE_GRAPH_PREWARM_DELAY_MS', 250, { min: 0, max: 60_000 });
|
|
2633
|
+
const statuslineUsageWarmupDelayMs = envDelayMs('MIXDOG_STATUSLINE_USAGE_WARMUP_DELAY_MS', 800, { min: 0, max: 60_000 });
|
|
2634
|
+
// Idle keep-alive: re-fetch usage before the statusline's 10-min staleness cut
|
|
2635
|
+
// (LIVE_USAGE_SNAPSHOT_MAX_AGE_MS) so the usage segment does not disappear
|
|
2636
|
+
// while the session sits idle with no turns to trigger a refresh.
|
|
2637
|
+
const statuslineUsageRefreshDelayMs = envDelayMs('MIXDOG_STATUSLINE_USAGE_REFRESH_MS', 240_000, { min: 30_000, max: 540_000 });
|
|
1671
2638
|
const channelStartDelayMs = envDelayMs('MIXDOG_CHANNEL_START_DELAY_MS', 10_000, { min: 0, max: 120_000 });
|
|
1672
2639
|
const backgroundBusyRetryMs = envDelayMs('MIXDOG_BACKGROUND_BUSY_RETRY_MS', 1_000, { min: 50, max: 10_000 });
|
|
2640
|
+
const sessionPrewarmEnabled = !envFlag('MIXDOG_DISABLE_SESSION_PREWARM')
|
|
2641
|
+
&& (envFlag('MIXDOG_ENABLE_SESSION_PREWARM') || envPresent('MIXDOG_SESSION_PREWARM_DELAY_MS'));
|
|
2642
|
+
const providerWarmupEnabled = !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
|
|
2643
|
+
&& (
|
|
2644
|
+
envFlag('MIXDOG_ENABLE_PROVIDER_WARMUP')
|
|
2645
|
+
|| envFlag('MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN')
|
|
2646
|
+
|| envPresent('MIXDOG_PROVIDER_WARMUP_DELAY_MS')
|
|
2647
|
+
|| envPresent('MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS')
|
|
2648
|
+
);
|
|
2649
|
+
// Boot-time model-catalog prefetch is intentionally decoupled from the
|
|
2650
|
+
// heavier providerWarmupEnabled gate (which stays opt-in for provider
|
|
2651
|
+
// *init* side effects). Fetching the model list in the background after a
|
|
2652
|
+
// short delay is cheap, fire-and-forget, and unref'd, so it is ON by
|
|
2653
|
+
// default — otherwise the FIRST `/model` open always paid a cold full
|
|
2654
|
+
// network load. Operators can still disable it explicitly.
|
|
2655
|
+
const modelPrefetchEnabled = !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
|
|
2656
|
+
&& !envFlag('MIXDOG_DISABLE_MODEL_PREFETCH');
|
|
2657
|
+
const codeGraphPrewarmEnabled = !envFlag('MIXDOG_DISABLE_CODE_GRAPH_PREWARM');
|
|
2658
|
+
// Lazy code-graph prewarm (default ON): do NOT prewarm at startup / on cwd
|
|
2659
|
+
// change — that fired ~250ms after the first frame and, in a large tree,
|
|
2660
|
+
// burned a worker (and felt like a freeze) before the user did anything.
|
|
2661
|
+
// Instead prewarm ONCE on the first real turn, when a code lookup is actually
|
|
2662
|
+
// imminent. Operators who want the old eager behavior can set
|
|
2663
|
+
// MIXDOG_CODE_GRAPH_PREWARM_EAGER=1.
|
|
2664
|
+
const codeGraphPrewarmLazy = codeGraphPrewarmEnabled && !envFlag('MIXDOG_CODE_GRAPH_PREWARM_EAGER');
|
|
2665
|
+
let codeGraphFirstTurnPrewarmDone = false;
|
|
1673
2666
|
const modelMetaByRoute = new Map();
|
|
1674
2667
|
const notificationListeners = new Set();
|
|
1675
2668
|
let providerModelsCache = { models: null, at: 0 };
|
|
1676
2669
|
let providerModelsPromise = null;
|
|
2670
|
+
let searchProviderModelsCache = { models: null, at: 0 };
|
|
2671
|
+
let searchProviderModelsPromise = null;
|
|
2672
|
+
let usageDashboardCache = { dashboard: null, at: 0 };
|
|
2673
|
+
let usageDashboardPromise = null;
|
|
1677
2674
|
let providerSetupCache = { setup: null, at: 0 };
|
|
2675
|
+
let providerSetupQuickCache = { setup: null, at: 0 };
|
|
1678
2676
|
let providerSetupPromise = null;
|
|
1679
2677
|
let providerInitPromise = null;
|
|
1680
|
-
const PROVIDER_SETUP_CACHE_TTL_MS = 10_000;
|
|
1681
2678
|
let mcpFailures = [];
|
|
1682
2679
|
let preSessionToolSurface = null;
|
|
1683
2680
|
let contextStatusCacheKey = null;
|
|
@@ -1687,72 +2684,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1687
2684
|
hooks.emit('runtime:start', { cwd: currentCwd, provider: route.provider, model: route.model, toolMode: mode });
|
|
1688
2685
|
bootProfile('hooks:ready', { ms: (performance.now() - hooksStartedAt).toFixed(1) });
|
|
1689
2686
|
|
|
1690
|
-
function contextContentLength(content) {
|
|
1691
|
-
if (typeof content === 'string') return content.length;
|
|
1692
|
-
if (!Array.isArray(content)) {
|
|
1693
|
-
try { return JSON.stringify(content ?? '').length; } catch { return String(content ?? '').length; }
|
|
1694
|
-
}
|
|
1695
|
-
let length = 0;
|
|
1696
|
-
for (const part of content) {
|
|
1697
|
-
if (typeof part === 'string') length += part.length;
|
|
1698
|
-
else if (typeof part?.text === 'string') length += part.text.length;
|
|
1699
|
-
else {
|
|
1700
|
-
try { length += JSON.stringify(part ?? '').length; } catch { length += String(part ?? '').length; }
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
return length;
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
function sameContextStatusKey(a, b) {
|
|
1707
|
-
if (!a || !b || a.length !== b.length) return false;
|
|
1708
|
-
for (let i = 0; i < a.length; i++) {
|
|
1709
|
-
if (!Object.is(a[i], b[i])) return false;
|
|
1710
|
-
}
|
|
1711
|
-
return true;
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
function buildContextStatusCacheKey(messages, tools, bridgeMode) {
|
|
1715
|
-
const lastMessage = messages[messages.length - 1] || null;
|
|
1716
|
-
const compaction = session?.compaction || null;
|
|
1717
|
-
return [
|
|
1718
|
-
session?.id || null,
|
|
1719
|
-
route.provider,
|
|
1720
|
-
route.model,
|
|
1721
|
-
currentCwd,
|
|
1722
|
-
mode,
|
|
1723
|
-
bridgeMode,
|
|
1724
|
-
messages,
|
|
1725
|
-
messages.length,
|
|
1726
|
-
lastMessage,
|
|
1727
|
-
lastMessage?.role || null,
|
|
1728
|
-
lastMessage?.content || null,
|
|
1729
|
-
contextContentLength(lastMessage?.content),
|
|
1730
|
-
Array.isArray(lastMessage?.toolCalls) ? lastMessage.toolCalls.length : 0,
|
|
1731
|
-
tools,
|
|
1732
|
-
tools.length,
|
|
1733
|
-
session?.contextWindow || null,
|
|
1734
|
-
session?.rawContextWindow || null,
|
|
1735
|
-
session?.effectiveContextWindowPercent || null,
|
|
1736
|
-
session?.lastContextTokens || 0,
|
|
1737
|
-
session?.lastContextTokensUpdatedAt || 0,
|
|
1738
|
-
session?.lastContextTokensStaleAfterCompact === true,
|
|
1739
|
-
session?.lastInputTokens || 0,
|
|
1740
|
-
session?.lastOutputTokens || 0,
|
|
1741
|
-
session?.lastCachedReadTokens || 0,
|
|
1742
|
-
session?.lastCacheWriteTokens || 0,
|
|
1743
|
-
session?.totalInputTokens || 0,
|
|
1744
|
-
session?.totalOutputTokens || 0,
|
|
1745
|
-
session?.totalCachedReadTokens || 0,
|
|
1746
|
-
session?.totalCacheWriteTokens || 0,
|
|
1747
|
-
session?.compactBoundaryTokens || 0,
|
|
1748
|
-
compaction,
|
|
1749
|
-
compaction?.lastChangedAt || 0,
|
|
1750
|
-
compaction?.lastCompactAt || 0,
|
|
1751
|
-
compaction?.boundaryTokens || 0,
|
|
1752
|
-
compaction?.triggerTokens || 0,
|
|
1753
|
-
];
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
2687
|
function mcpTransportLabel(cfg = {}) {
|
|
1757
2688
|
if (cfg.autoDetect) return `autoDetect:${cfg.autoDetect}`;
|
|
1758
2689
|
if (cfg.transport === 'http' || cfg.url) return 'http';
|
|
@@ -1762,23 +2693,42 @@ export async function createMixdogSessionRuntime({
|
|
|
1762
2693
|
|
|
1763
2694
|
function emitRuntimeNotification(content, meta = {}) {
|
|
1764
2695
|
const text = String(content || '').trim();
|
|
1765
|
-
if (!text) return;
|
|
2696
|
+
if (!text) return false;
|
|
1766
2697
|
const event = { content: text, meta: meta && typeof meta === 'object' ? meta : {} };
|
|
2698
|
+
let handled = false;
|
|
1767
2699
|
for (const listener of [...notificationListeners]) {
|
|
1768
|
-
try {
|
|
2700
|
+
try {
|
|
2701
|
+
if (listener(event) === true) handled = true;
|
|
2702
|
+
} catch {}
|
|
1769
2703
|
}
|
|
2704
|
+
return handled;
|
|
1770
2705
|
}
|
|
1771
2706
|
|
|
1772
2707
|
function notifyFnForSession(callerSessionId) {
|
|
1773
2708
|
return (text, meta = {}) => {
|
|
1774
|
-
const
|
|
1775
|
-
|
|
1776
|
-
// TUI sessions
|
|
1777
|
-
// onNotification
|
|
1778
|
-
//
|
|
1779
|
-
|
|
1780
|
-
|
|
2709
|
+
const handledByRuntimeListener = emitRuntimeNotification(text, meta);
|
|
2710
|
+
let enqueued = false;
|
|
2711
|
+
// TUI sessions consume raw execution notifications for UI/task cards via
|
|
2712
|
+
// onNotification, but those raw envelopes are internal-only in pending
|
|
2713
|
+
// drain. Always mirror terminal completions with a model-visible wrapper
|
|
2714
|
+
// while keeping the raw text for UI display.
|
|
2715
|
+
if (callerSessionId && typeof mgr.enqueuePendingMessage === 'function'
|
|
2716
|
+
&& shouldPersistModelVisibleToolCompletion(text, meta)) {
|
|
2717
|
+
try {
|
|
2718
|
+
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
2719
|
+
if (visible) enqueued = mgr.enqueuePendingMessage(callerSessionId, visible) > 0;
|
|
2720
|
+
} catch {}
|
|
2721
|
+
}
|
|
2722
|
+
// Headless/API listeners may exist but not consume the event; preserve
|
|
2723
|
+
// the old fallback for non-terminal notifications only when unhandled.
|
|
2724
|
+
if (!enqueued && !handledByRuntimeListener && callerSessionId
|
|
2725
|
+
&& typeof mgr.enqueuePendingMessage === 'function') {
|
|
2726
|
+
try {
|
|
2727
|
+
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
2728
|
+
if (visible) enqueued = mgr.enqueuePendingMessage(callerSessionId, visible) > 0;
|
|
2729
|
+
} catch {}
|
|
1781
2730
|
}
|
|
2731
|
+
return enqueued || handledByRuntimeListener;
|
|
1782
2732
|
};
|
|
1783
2733
|
}
|
|
1784
2734
|
|
|
@@ -1840,40 +2790,92 @@ export async function createMixdogSessionRuntime({
|
|
|
1840
2790
|
};
|
|
1841
2791
|
}
|
|
1842
2792
|
|
|
2793
|
+
function scheduleCodeGraphPrewarm(delayMs = codeGraphPrewarmDelayMs, reason = 'cwd') {
|
|
2794
|
+
if (!codeGraphPrewarmEnabled) {
|
|
2795
|
+
bootProfile('code-graph:prewarm-skipped', { reason: 'disabled' });
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
if (closeRequested) return;
|
|
2799
|
+
codeGraphPrewarmQueuedCwd = currentCwd;
|
|
2800
|
+
if (codeGraphPrewarmTimer) return;
|
|
2801
|
+
codeGraphPrewarmTimer = setTimeout(() => {
|
|
2802
|
+
codeGraphPrewarmTimer = null;
|
|
2803
|
+
if (closeRequested) return;
|
|
2804
|
+
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
2805
|
+
bootProfile('code-graph:prewarm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
2806
|
+
scheduleCodeGraphPrewarm(backgroundBusyRetryMs, 'busy');
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
if (codeGraphPrewarmInFlight) {
|
|
2810
|
+
bootProfile('code-graph:prewarm-deferred', { reason: 'in-flight' });
|
|
2811
|
+
scheduleCodeGraphPrewarm(backgroundBusyRetryMs, 'in-flight');
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
const prewarmCwd = codeGraphPrewarmQueuedCwd || currentCwd;
|
|
2815
|
+
codeGraphPrewarmQueuedCwd = '';
|
|
2816
|
+
codeGraphPrewarmInFlight = true;
|
|
2817
|
+
const startedAt = performance.now();
|
|
2818
|
+
bootProfile('code-graph:prewarm:start', { cwd: prewarmCwd, reason });
|
|
2819
|
+
void getCodeGraphModule()
|
|
2820
|
+
.then((mod) => {
|
|
2821
|
+
if (typeof mod?.prewarmCodeGraphIfProject !== 'function') return false;
|
|
2822
|
+
return mod.prewarmCodeGraphIfProject(prewarmCwd);
|
|
2823
|
+
})
|
|
2824
|
+
.then((scheduled) => bootProfile(scheduled ? 'code-graph:prewarm:scheduled' : 'code-graph:prewarm:no-project', {
|
|
2825
|
+
cwd: prewarmCwd,
|
|
2826
|
+
ms: (performance.now() - startedAt).toFixed(1),
|
|
2827
|
+
}))
|
|
2828
|
+
.catch((error) => bootProfile('code-graph:prewarm:failed', {
|
|
2829
|
+
cwd: prewarmCwd,
|
|
2830
|
+
ms: (performance.now() - startedAt).toFixed(1),
|
|
2831
|
+
error: error?.message || String(error),
|
|
2832
|
+
}))
|
|
2833
|
+
.finally(() => {
|
|
2834
|
+
codeGraphPrewarmInFlight = false;
|
|
2835
|
+
if (codeGraphPrewarmQueuedCwd && !closeRequested) {
|
|
2836
|
+
scheduleCodeGraphPrewarm(backgroundBusyRetryMs, 'queued');
|
|
2837
|
+
}
|
|
2838
|
+
});
|
|
2839
|
+
}, delayMs);
|
|
2840
|
+
codeGraphPrewarmTimer.unref?.();
|
|
2841
|
+
}
|
|
2842
|
+
|
|
1843
2843
|
function applyResolvedCwd(nextCwd, { markRefresh = true } = {}) {
|
|
1844
2844
|
const resolved = resolve(nextCwd);
|
|
1845
2845
|
const stat = statSync(resolved);
|
|
1846
2846
|
if (!stat.isDirectory()) throw new Error(`cwd: not a directory: ${resolved}`);
|
|
1847
2847
|
const changed = resolve(currentCwd) !== resolved;
|
|
1848
2848
|
currentCwd = resolved;
|
|
1849
|
-
ensureProjectMixdogMd({ cwd: currentCwd });
|
|
1850
2849
|
process.env.MIXDOG_SESSION_CWD = currentCwd;
|
|
1851
2850
|
writeLastSessionCwd(currentCwd);
|
|
1852
2851
|
if (session) session.cwd = currentCwd;
|
|
1853
|
-
|
|
2852
|
+
// cwd changes NEVER recreate the session: a mid-conversation cwd switch must
|
|
2853
|
+
// preserve the full message history (and the BP1–BP3 prompt cache). We only
|
|
2854
|
+
// retarget the live session's cwd in place; tool execution already reads the
|
|
2855
|
+
// current cwd per turn. `cwd` is intentionally absent from the prompt
|
|
2856
|
+
// context (see composeSystemPrompt), so there is nothing prompt-side to
|
|
2857
|
+
// refresh either. `markRefresh`/`changed` are kept only for signature
|
|
2858
|
+
// compatibility with existing callers.
|
|
2859
|
+
void markRefresh;
|
|
2860
|
+
// Lazy mode: before the first turn (e.g. the initial project-selection
|
|
2861
|
+
// cwd set), do NOT prewarm — that is exactly the post-first-frame freeze
|
|
2862
|
+
// we are avoiding. Once a turn has run, an in-session cwd switch DOES
|
|
2863
|
+
// prewarm the new dir, since a lookup there is now likely.
|
|
2864
|
+
if (codeGraphPrewarmLazy && !codeGraphFirstTurnPrewarmDone) {
|
|
2865
|
+
bootProfile('code-graph:prewarm-lazy', { reason: 'cwd-deferred-to-first-turn' });
|
|
2866
|
+
} else {
|
|
2867
|
+
scheduleCodeGraphPrewarm(changed ? 0 : codeGraphPrewarmDelayMs, changed ? 'cwd-change' : 'cwd');
|
|
2868
|
+
}
|
|
1854
2869
|
return currentCwd;
|
|
1855
2870
|
}
|
|
1856
2871
|
|
|
1857
2872
|
async function refreshSessionForCwdIfNeeded(reason = 'cwd-change') {
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
session = null;
|
|
2873
|
+
// No-op: cwd changes are applied in place by applyResolvedCwd and never
|
|
2874
|
+
// tear down the session. Retained as a stable hook for ask()'s pre-turn
|
|
2875
|
+
// call so the surrounding turn flow is unchanged.
|
|
2876
|
+
void reason;
|
|
1863
2877
|
sessionNeedsCwdRefresh = false;
|
|
1864
|
-
return
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
function buildWorkspaceContext() {
|
|
1868
|
-
try {
|
|
1869
|
-
return formatWorkspaceSessionContext(workspaceRouter.snapshot(currentCwd));
|
|
1870
|
-
} catch (error) {
|
|
1871
|
-
return [
|
|
1872
|
-
'# Workspace',
|
|
1873
|
-
`current cwd: ${currentCwd}`,
|
|
1874
|
-
`project candidates: unavailable (${error?.message || String(error)})`,
|
|
1875
|
-
].join('\n');
|
|
1876
|
-
}
|
|
2878
|
+
return session;
|
|
1877
2879
|
}
|
|
1878
2880
|
|
|
1879
2881
|
function skillContent(name) {
|
|
@@ -1884,6 +2886,12 @@ export async function createMixdogSessionRuntime({
|
|
|
1884
2886
|
return { name, content };
|
|
1885
2887
|
}
|
|
1886
2888
|
|
|
2889
|
+
function skillToolContent(name) {
|
|
2890
|
+
const skill = skillContent(name);
|
|
2891
|
+
const escapedName = String(skill.name || '').replace(/[<>&]/g, (ch) => ({ '<': '<', '>': '>', '&': '&' }[ch]));
|
|
2892
|
+
return `<skill>\n<name>${escapedName}</name>\n${skill.content}\n</skill>`;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
1887
2895
|
function addProjectSkill(input = {}) {
|
|
1888
2896
|
const name = clean(input.name).replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1889
2897
|
if (!name) throw new Error('skill name is required');
|
|
@@ -1996,30 +3004,25 @@ export async function createMixdogSessionRuntime({
|
|
|
1996
3004
|
return { name, config: { command, args, cwd: resolvedCwd } };
|
|
1997
3005
|
}
|
|
1998
3006
|
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2001
|
-
})();
|
|
2002
|
-
const bridgeStartedAt = performance.now();
|
|
2003
|
-
const bridge = createStandaloneBridge({
|
|
3007
|
+
const agentToolStartedAt = performance.now();
|
|
3008
|
+
const agentTool = createStandaloneAgent({
|
|
2004
3009
|
cfgMod,
|
|
2005
3010
|
reg,
|
|
2006
3011
|
mgr,
|
|
2007
3012
|
dataDir: cfgMod.getPluginData(),
|
|
2008
3013
|
cwd,
|
|
2009
|
-
defaultMode: persistedBridgeMode ?? 'async',
|
|
2010
3014
|
});
|
|
2011
|
-
bootProfile('
|
|
2012
|
-
const
|
|
3015
|
+
bootProfile('agent:ready', { ms: (performance.now() - agentToolStartedAt).toFixed(1) });
|
|
3016
|
+
const agentStatusState = () => {
|
|
2013
3017
|
try {
|
|
2014
|
-
const status =
|
|
3018
|
+
const status = agentTool.getStatus?.({ clientHostPid: session?.clientHostPid || process.pid }) || {};
|
|
2015
3019
|
return {
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
bridgeScope: status.scope || null,
|
|
3020
|
+
agentWorkers: Array.isArray(status.workers) ? status.workers : [],
|
|
3021
|
+
agentJobs: Array.isArray(status.jobs) ? status.jobs : [],
|
|
3022
|
+
agentScope: status.scope || null,
|
|
2020
3023
|
};
|
|
2021
3024
|
} catch {
|
|
2022
|
-
return {
|
|
3025
|
+
return { agentWorkers: [], agentJobs: [], agentScope: null };
|
|
2023
3026
|
}
|
|
2024
3027
|
};
|
|
2025
3028
|
const channelsStartedAt = performance.now();
|
|
@@ -2028,18 +3031,28 @@ export async function createMixdogSessionRuntime({
|
|
|
2028
3031
|
rootDir: STANDALONE_ROOT,
|
|
2029
3032
|
dataDir: cfgMod.getPluginData(),
|
|
2030
3033
|
cwd,
|
|
3034
|
+
onNotify: (msg) => {
|
|
3035
|
+
if (msg?.method !== 'notifications/claude/channel') return;
|
|
3036
|
+
const params = msg?.params && typeof msg.params === 'object' ? msg.params : {};
|
|
3037
|
+
const meta = params.meta && typeof params.meta === 'object' ? params.meta : {};
|
|
3038
|
+
if (meta.silent_to_agent === true || meta.silent_to_agent === 'true') return;
|
|
3039
|
+
const instruction = typeof meta.instruction === 'string' ? meta.instruction.trim() : '';
|
|
3040
|
+
const content = instruction || String(params.content || '').trim();
|
|
3041
|
+
emitRuntimeNotification(content, meta);
|
|
3042
|
+
},
|
|
2031
3043
|
});
|
|
2032
3044
|
bootProfile('channels:worker-ready', { ms: (performance.now() - channelsStartedAt).toFixed(1) });
|
|
2033
3045
|
const toolsStartedAt = performance.now();
|
|
2034
3046
|
const standaloneTools = [
|
|
2035
3047
|
TOOL_SEARCH_TOOL,
|
|
3048
|
+
SKILL_TOOL,
|
|
2036
3049
|
CWD_TOOL,
|
|
2037
3050
|
EXPLORE_TOOL,
|
|
2038
3051
|
...(searchToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'search' || tool?.name === 'web_fetch'),
|
|
2039
3052
|
...(memoryToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'recall' || tool?.name === 'memory'),
|
|
2040
3053
|
...(channelToolDefs?.TOOL_DEFS || []).filter((tool) => channels.isChannelTool(tool?.name)),
|
|
2041
3054
|
...(codeGraphToolDefs?.CODE_GRAPH_TOOL_DEFS || []).filter((tool) => tool?.name === 'code_graph'),
|
|
2042
|
-
...
|
|
3055
|
+
...agentTool.tools,
|
|
2043
3056
|
PROVIDER_STATUS_TOOL,
|
|
2044
3057
|
CHANNEL_STATUS_TOOL,
|
|
2045
3058
|
].map(applyStandaloneToolDefaults);
|
|
@@ -2060,7 +3073,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2060
3073
|
: [];
|
|
2061
3074
|
const tools = filterDisallowedTools(previewTools, LEAD_DISALLOWED_TOOLS);
|
|
2062
3075
|
const surface = { tools: Array.isArray(tools) ? tools.slice() : [] };
|
|
2063
|
-
applyDeferredToolSurface(surface, mode, standaloneTools);
|
|
3076
|
+
applyDeferredToolSurface(surface, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
2064
3077
|
return surface;
|
|
2065
3078
|
}
|
|
2066
3079
|
|
|
@@ -2075,7 +3088,11 @@ export async function createMixdogSessionRuntime({
|
|
|
2075
3088
|
const selected = Array.isArray(preSessionToolSurface.deferredSelectedTools)
|
|
2076
3089
|
? preSessionToolSurface.deferredSelectedTools
|
|
2077
3090
|
: [];
|
|
2078
|
-
|
|
3091
|
+
const discovered = Array.isArray(preSessionToolSurface.deferredDiscoveredTools)
|
|
3092
|
+
? preSessionToolSurface.deferredDiscoveredTools
|
|
3093
|
+
: [];
|
|
3094
|
+
const replay = [...new Set([...selected, ...discovered])];
|
|
3095
|
+
if (replay.length) selectDeferredTools(session, replay, deferredSurfaceModeForLead(mode));
|
|
2079
3096
|
}
|
|
2080
3097
|
internalTools.setInternalToolsProvider({
|
|
2081
3098
|
tools: standaloneTools,
|
|
@@ -2091,35 +3108,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2091
3108
|
routingSessionId: callerSessionId,
|
|
2092
3109
|
clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
|
|
2093
3110
|
notifyFn: notifyFnForSession(callerSessionId),
|
|
2094
|
-
|
|
2095
|
-
? async (searchArgs) => {
|
|
2096
|
-
const query = Array.isArray(searchArgs.keywords)
|
|
2097
|
-
? searchArgs.keywords.join('\n')
|
|
2098
|
-
: String(searchArgs.keywords || searchArgs.query || '');
|
|
2099
|
-
const prompt = searchArgs.prompt || [
|
|
2100
|
-
'Perform a concise web research task for Mixdog search.',
|
|
2101
|
-
'',
|
|
2102
|
-
`Query: ${query}`,
|
|
2103
|
-
searchArgs.site ? `Site/domain restriction: ${searchArgs.site}` : null,
|
|
2104
|
-
searchArgs.type ? `Search type: ${searchArgs.type}` : null,
|
|
2105
|
-
searchArgs.locale ? `Locale: ${typeof searchArgs.locale === 'string' ? searchArgs.locale : JSON.stringify(searchArgs.locale)}` : null,
|
|
2106
|
-
`Max results: ${Math.max(1, Math.min(20, Number(searchArgs.maxResults) || 10))}`,
|
|
2107
|
-
'',
|
|
2108
|
-
'Return a short answer first, then cite useful results as title + URL + one-line snippet.',
|
|
2109
|
-
'Do not edit files.',
|
|
2110
|
-
].filter(Boolean).join('\n');
|
|
2111
|
-
const rendered = await bridge.execute({
|
|
2112
|
-
type: 'spawn',
|
|
2113
|
-
agent: 'web-researcher',
|
|
2114
|
-
tag: `search_${Date.now().toString(36)}`,
|
|
2115
|
-
prompt,
|
|
2116
|
-
cwd: callerCwd,
|
|
2117
|
-
wait: true,
|
|
2118
|
-
firstResponseTimeoutMs: Number.isFinite(Number(searchArgs.firstResponseTimeoutMs)) ? Number(searchArgs.firstResponseTimeoutMs) : 120_000,
|
|
2119
|
-
idleTimeoutMs: Number.isFinite(Number(searchArgs.idleTimeoutMs)) ? Number(searchArgs.idleTimeoutMs) : 30 * 60_000,
|
|
2120
|
-
}, { invocationSource: 'user-command', callerCwd });
|
|
2121
|
-
return String(rendered || '').replace(/^bridge result[^\n]*\n/i, '').trim();
|
|
2122
|
-
}
|
|
3111
|
+
nativeSearch: name === 'search'
|
|
3112
|
+
? async (searchArgs) => runNativeWebSearch(searchArgs, { signal: callerCtx?.signal || session?.controller?.signal })
|
|
2123
3113
|
: undefined,
|
|
2124
3114
|
});
|
|
2125
3115
|
}
|
|
@@ -2133,7 +3123,9 @@ export async function createMixdogSessionRuntime({
|
|
|
2133
3123
|
if (!codeGraphMod?.executeCodeGraphTool) throw new Error('code_graph runtime is not available');
|
|
2134
3124
|
return await codeGraphMod.executeCodeGraphTool(name, args || {}, args?.cwd || callerCwd);
|
|
2135
3125
|
}
|
|
2136
|
-
if (name === 'tool_search')
|
|
3126
|
+
if (name === 'tool_search') {
|
|
3127
|
+
return renderToolSearch(args, activeToolSurface(), mode);
|
|
3128
|
+
}
|
|
2137
3129
|
if (name === 'explore') {
|
|
2138
3130
|
const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
|
|
2139
3131
|
return await runExplore(args || {}, {
|
|
@@ -2153,9 +3145,12 @@ export async function createMixdogSessionRuntime({
|
|
|
2153
3145
|
}
|
|
2154
3146
|
return JSON.stringify({ cwd: currentCwd, sessionId: session?.id || null }, null, 2);
|
|
2155
3147
|
}
|
|
2156
|
-
if (name === '
|
|
3148
|
+
if (name === 'Skill') {
|
|
3149
|
+
return skillToolContent(args?.name);
|
|
3150
|
+
}
|
|
3151
|
+
if (name === 'agent') {
|
|
2157
3152
|
const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
|
|
2158
|
-
return await
|
|
3153
|
+
return await agentTool.execute(args, {
|
|
2159
3154
|
callerCwd,
|
|
2160
3155
|
invocationSource: 'model-tool',
|
|
2161
3156
|
callerSessionId,
|
|
@@ -2164,9 +3159,12 @@ export async function createMixdogSessionRuntime({
|
|
|
2164
3159
|
notifyFn: notifyFnForSession(callerSessionId),
|
|
2165
3160
|
});
|
|
2166
3161
|
}
|
|
2167
|
-
if (name === 'provider_status') return renderProviderStatus(
|
|
3162
|
+
if (name === 'provider_status') return renderProviderStatus(displayConfig());
|
|
2168
3163
|
if (name === 'channel_status') return renderChannelStatus();
|
|
2169
|
-
if (channels.isChannelTool(name))
|
|
3164
|
+
if (channels.isChannelTool(name)) {
|
|
3165
|
+
if (!channelsEnabled()) throw new Error('channels are disabled in settings');
|
|
3166
|
+
return await channels.execute(name, args || {});
|
|
3167
|
+
}
|
|
2170
3168
|
throw new Error(`unknown standalone internal tool: ${name}`);
|
|
2171
3169
|
},
|
|
2172
3170
|
});
|
|
@@ -2185,16 +3183,81 @@ export async function createMixdogSessionRuntime({
|
|
|
2185
3183
|
function invalidateProviderCaches() {
|
|
2186
3184
|
providerModelsCache = { models: null, at: 0 };
|
|
2187
3185
|
providerModelsPromise = null;
|
|
3186
|
+
searchProviderModelsCache = { models: null, at: 0 };
|
|
3187
|
+
searchProviderModelsPromise = null;
|
|
3188
|
+
usageDashboardCache = { dashboard: null, at: 0 };
|
|
3189
|
+
usageDashboardPromise = null;
|
|
2188
3190
|
providerSetupCache = { setup: null, at: 0 };
|
|
3191
|
+
providerSetupQuickCache = { setup: null, at: 0 };
|
|
2189
3192
|
providerSetupPromise = null;
|
|
2190
3193
|
providerInitPromise = null;
|
|
2191
3194
|
modelMetaByRoute.clear();
|
|
2192
3195
|
}
|
|
2193
3196
|
|
|
3197
|
+
function adoptConfig(nextConfig, { hasSecrets = configHasSecrets } = {}) {
|
|
3198
|
+
config = nextConfig;
|
|
3199
|
+
configHasSecrets = hasSecrets === true;
|
|
3200
|
+
setConfiguredShell(normalizeSystemShellConfig(config.shell).command);
|
|
3201
|
+
searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
3202
|
+
return config;
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
// Persisting mixdog-config.json is heavy: it takes a cross-process file
|
|
3206
|
+
// lock, does an atomic temp+rename, and on win32 shells out to icacls
|
|
3207
|
+
// (an external process, multiple times per write) to enforce an owner-only
|
|
3208
|
+
// ACL. Doing that synchronously on EVERY option toggle froze the TUI main
|
|
3209
|
+
// thread, so adjusting a picker felt laggy and unresponsive. We split the
|
|
3210
|
+
// two concerns: adopt the new config in-memory IMMEDIATELY (so getProfile/
|
|
3211
|
+
// getAutoClear/etc. and the UI read fresh values on the same tick), and
|
|
3212
|
+
// DEBOUNCE the actual disk write so a burst of toggles collapses into a
|
|
3213
|
+
// single persist after input settles. The security model is unchanged —
|
|
3214
|
+
// cfgMod.saveConfig still strips apiKey (keychain-only) and still applies
|
|
3215
|
+
// the owner-only ACL; we only changed WHEN it runs, never HOW.
|
|
3216
|
+
let pendingConfigToSave = null;
|
|
3217
|
+
let configSaveTimer = null;
|
|
3218
|
+
const CONFIG_SAVE_DEBOUNCE_MS = 150;
|
|
3219
|
+
function flushConfigSave() {
|
|
3220
|
+
if (configSaveTimer) {
|
|
3221
|
+
clearTimeout(configSaveTimer);
|
|
3222
|
+
configSaveTimer = null;
|
|
3223
|
+
}
|
|
3224
|
+
if (pendingConfigToSave === null) return;
|
|
3225
|
+
const snapshot = pendingConfigToSave;
|
|
3226
|
+
pendingConfigToSave = null;
|
|
3227
|
+
try {
|
|
3228
|
+
cfgMod.saveConfig(snapshot);
|
|
3229
|
+
} catch (err) {
|
|
3230
|
+
process.stderr.write(`[config] debounced saveConfig failed: ${err?.message || err}\n`);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
function saveConfigAndAdopt(nextConfig, { hasSecrets = configHasSecrets } = {}) {
|
|
3234
|
+
// In-memory adopt is synchronous and first so callers that read back the
|
|
3235
|
+
// value immediately (e.g. setProfile -> getProfile) see the new state.
|
|
3236
|
+
const adopted = adoptConfig(nextConfig, { hasSecrets });
|
|
3237
|
+
// Persist the adopted object; coalesce rapid successive changes into one
|
|
3238
|
+
// disk write after CONFIG_SAVE_DEBOUNCE_MS of quiet.
|
|
3239
|
+
pendingConfigToSave = config;
|
|
3240
|
+
if (configSaveTimer) clearTimeout(configSaveTimer);
|
|
3241
|
+
configSaveTimer = setTimeout(flushConfigSave, CONFIG_SAVE_DEBOUNCE_MS);
|
|
3242
|
+
configSaveTimer.unref?.();
|
|
3243
|
+
return adopted;
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
function reloadFullConfig() {
|
|
3247
|
+
// A pending debounced write holds the only copy of the latest change.
|
|
3248
|
+
// Flush it before re-reading from disk so loadConfig() observes (and the
|
|
3249
|
+
// subsequent adopt preserves) that change instead of reverting to a stale
|
|
3250
|
+
// on-disk snapshot.
|
|
3251
|
+
flushConfigSave();
|
|
3252
|
+
return adoptConfig(cfgMod.loadConfig(), { hasSecrets: true });
|
|
3253
|
+
}
|
|
3254
|
+
|
|
2194
3255
|
function ensureFullConfig() {
|
|
2195
3256
|
if (configHasSecrets) return config;
|
|
2196
|
-
|
|
2197
|
-
|
|
3257
|
+
return reloadFullConfig();
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
function displayConfig() {
|
|
2198
3261
|
return config;
|
|
2199
3262
|
}
|
|
2200
3263
|
|
|
@@ -2207,6 +3270,19 @@ export async function createMixdogSessionRuntime({
|
|
|
2207
3270
|
return ensureFullConfig();
|
|
2208
3271
|
}
|
|
2209
3272
|
|
|
3273
|
+
function refreshStatuslineUsageSnapshot(routeLike = {}) {
|
|
3274
|
+
const providerId = clean(routeLike.provider);
|
|
3275
|
+
const modelId = clean(routeLike.model);
|
|
3276
|
+
if (!providerId || !providerId.includes('oauth')) return;
|
|
3277
|
+
const providerObj = reg.getProvider(providerId);
|
|
3278
|
+
if (!providerObj) return;
|
|
3279
|
+
void fetchOAuthUsageSnapshot({ provider: providerId, model: modelId }, providerObj, (message) => {
|
|
3280
|
+
if (process.env.MIXDOG_STATUSLINE_TRACE) {
|
|
3281
|
+
try { process.stderr.write(`[statusline] ${message}\n`); } catch {}
|
|
3282
|
+
}
|
|
3283
|
+
}).catch(() => {});
|
|
3284
|
+
}
|
|
3285
|
+
|
|
2210
3286
|
async function ensureProvidersReady(providerConfig = config.providers || {}) {
|
|
2211
3287
|
if (providerInitPromise) return await providerInitPromise;
|
|
2212
3288
|
providerInitPromise = reg.initProviders(providerConfig)
|
|
@@ -2216,13 +3292,23 @@ export async function createMixdogSessionRuntime({
|
|
|
2216
3292
|
return await providerInitPromise;
|
|
2217
3293
|
}
|
|
2218
3294
|
|
|
2219
|
-
async function cachedProviderSetup({ force = false } = {}) {
|
|
2220
|
-
|
|
2221
|
-
if (!force && providerSetupCache.setup && now - providerSetupCache.at < PROVIDER_SETUP_CACHE_TTL_MS) {
|
|
3295
|
+
async function cachedProviderSetup({ force = false, quick = false } = {}) {
|
|
3296
|
+
if (!force && providerSetupCache.setup) {
|
|
2222
3297
|
return providerSetupCache.setup;
|
|
2223
3298
|
}
|
|
2224
|
-
if (
|
|
2225
|
-
|
|
3299
|
+
if (quick) {
|
|
3300
|
+
if (!force && providerSetupQuickCache.setup) {
|
|
3301
|
+
return providerSetupQuickCache.setup;
|
|
3302
|
+
}
|
|
3303
|
+
const setup = await providerSetup(displayConfig(), { detectLocal: false, checkSecrets: false });
|
|
3304
|
+
providerSetupQuickCache = { setup, at: Date.now() };
|
|
3305
|
+
if (!providerSetupPromise && !providerSetupWarmupTimer && !closeRequested) {
|
|
3306
|
+
scheduleProviderSetupWarmup(0);
|
|
3307
|
+
}
|
|
3308
|
+
return setup;
|
|
3309
|
+
}
|
|
3310
|
+
if (providerSetupPromise) return await providerSetupPromise;
|
|
3311
|
+
providerSetupPromise = providerSetup(displayConfig(), { detectLocal: true })
|
|
2226
3312
|
.then((setup) => {
|
|
2227
3313
|
providerSetupCache = { setup, at: Date.now() };
|
|
2228
3314
|
return setup;
|
|
@@ -2370,9 +3456,10 @@ function parsedProviderModelVersion(id) {
|
|
|
2370
3456
|
description: m.description || '',
|
|
2371
3457
|
supportsVision: m.supportsVision === true,
|
|
2372
3458
|
supportsFunctionCalling: m.supportsFunctionCalling === true,
|
|
3459
|
+
supportsWebSearch: searchCapableFor(name, m),
|
|
2373
3460
|
supportsPromptCaching: m.supportsPromptCaching === true,
|
|
2374
3461
|
supportsReasoning: m.supportsReasoning === true,
|
|
2375
|
-
reasoningLevels: Array.isArray(m.reasoningLevels) ? m.reasoningLevels :
|
|
3462
|
+
reasoningLevels: Array.isArray(m.reasoningLevels) ? m.reasoningLevels : undefined,
|
|
2376
3463
|
reasoningOptions: Array.isArray(m.reasoningOptions) ? m.reasoningOptions : [],
|
|
2377
3464
|
reasoningContentField: m.reasoningContentField || null,
|
|
2378
3465
|
mode: m.mode || null,
|
|
@@ -2394,40 +3481,265 @@ function parsedProviderModelVersion(id) {
|
|
|
2394
3481
|
return sortProviderModels((rows || []).map(hydrateProviderModelRow));
|
|
2395
3482
|
}
|
|
2396
3483
|
|
|
2397
|
-
|
|
3484
|
+
function quickProviderModelRows() {
|
|
3485
|
+
const pickerConfig = displayConfig();
|
|
3486
|
+
const rows = [];
|
|
3487
|
+
const seen = new Set();
|
|
3488
|
+
const addRoute = (routeLike = {}) => {
|
|
3489
|
+
const provider = clean(routeLike.provider);
|
|
3490
|
+
const model = clean(routeLike.model);
|
|
3491
|
+
if (!provider || !model) return;
|
|
3492
|
+
const key = `${provider}:${model}`;
|
|
3493
|
+
if (seen.has(key)) return;
|
|
3494
|
+
seen.add(key);
|
|
3495
|
+
const row = providerModelCacheRow(provider, {
|
|
3496
|
+
id: model,
|
|
3497
|
+
name: routeLike.modelDisplay || routeLike.display || model,
|
|
3498
|
+
display: routeLike.modelDisplay || routeLike.display || model,
|
|
3499
|
+
latest: routeLike.latest === true,
|
|
3500
|
+
supportsReasoning: !!routeLike.effort,
|
|
3501
|
+
mode: 'chat',
|
|
3502
|
+
});
|
|
3503
|
+
rows.push(row);
|
|
3504
|
+
modelMetaByRoute.set(modelMetaKey(provider, model), row);
|
|
3505
|
+
};
|
|
3506
|
+
|
|
3507
|
+
addRoute(route);
|
|
3508
|
+
for (const preset of pickerConfig.presets || []) addRoute(preset);
|
|
3509
|
+
for (const workflowRoute of Object.values(pickerConfig.workflowRoutes || {})) addRoute(workflowRoute);
|
|
3510
|
+
for (const agentRoute of Object.values(pickerConfig.agents || {})) addRoute(agentRoute);
|
|
3511
|
+
return providerModelsFromCacheRows(rows);
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
function addQuickSearchModel(rows, seen, provider, model) {
|
|
3515
|
+
const providerName = normalizeSearchProviderId(provider);
|
|
3516
|
+
const modelId = clean(model?.id || model);
|
|
3517
|
+
if (!providerName || !modelId || !isSearchCapableProvider(providerName)) return;
|
|
3518
|
+
const key = `${providerName}:${modelId}`;
|
|
3519
|
+
if (seen.has(key)) return;
|
|
3520
|
+
const row = providerModelCacheRow(providerName, {
|
|
3521
|
+
id: modelId,
|
|
3522
|
+
name: model?.name || model?.display || modelId,
|
|
3523
|
+
display: model?.display || model?.name || modelId,
|
|
3524
|
+
contextWindow: model?.contextWindow || null,
|
|
3525
|
+
outputTokens: model?.outputTokens || null,
|
|
3526
|
+
latest: model?.latest === true,
|
|
3527
|
+
supportsWebSearch: true,
|
|
3528
|
+
supportsFunctionCalling: model?.supportsFunctionCalling === true,
|
|
3529
|
+
supportsPromptCaching: model?.supportsPromptCaching === true,
|
|
3530
|
+
supportsReasoning: model?.supportsReasoning === true,
|
|
3531
|
+
reasoningLevels: Array.isArray(model?.reasoningLevels) ? model.reasoningLevels : undefined,
|
|
3532
|
+
reasoningOptions: Array.isArray(model?.reasoningOptions) ? model.reasoningOptions : [],
|
|
3533
|
+
mode: 'chat',
|
|
3534
|
+
});
|
|
3535
|
+
if (row.supportsWebSearch !== true) return;
|
|
3536
|
+
seen.add(key);
|
|
3537
|
+
rows.push({
|
|
3538
|
+
...row,
|
|
3539
|
+
provider: providerName,
|
|
3540
|
+
searchCapable: true,
|
|
3541
|
+
searchToolType: row.searchToolType || 'web_search',
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
function addDefaultSearchModel(rows, seen = new Set()) {
|
|
3546
|
+
const mainModel = currentMainSearchModelMeta();
|
|
3547
|
+
if (!mainModel || !searchCapableFor(route.provider, mainModel)) return;
|
|
3548
|
+
const key = `${SEARCH_DEFAULT_PROVIDER}:${SEARCH_DEFAULT_MODEL}`;
|
|
3549
|
+
if (seen.has(key)) return;
|
|
3550
|
+
seen.add(key);
|
|
3551
|
+
rows.push({
|
|
3552
|
+
id: SEARCH_DEFAULT_MODEL,
|
|
3553
|
+
provider: SEARCH_DEFAULT_PROVIDER,
|
|
3554
|
+
display: 'Default',
|
|
3555
|
+
name: 'Default',
|
|
3556
|
+
description: `Use current main model: ${route.provider}/${route.model}`,
|
|
3557
|
+
supportsWebSearch: true,
|
|
3558
|
+
searchCapable: true,
|
|
3559
|
+
searchToolType: 'web_search',
|
|
3560
|
+
mode: 'chat',
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
function quickSearchProviderModelRows() {
|
|
3565
|
+
const pickerConfig = displayConfig();
|
|
3566
|
+
const rows = [];
|
|
3567
|
+
const seen = new Set();
|
|
3568
|
+
addDefaultSearchModel(rows, seen);
|
|
3569
|
+
for (const [name, providerConfig] of Object.entries(pickerConfig.providers || {})) {
|
|
3570
|
+
const providerName = normalizeSearchProviderId(name);
|
|
3571
|
+
if (!providerConfig?.enabled || !isSearchCapableProvider(providerName)) continue;
|
|
3572
|
+
for (const model of QUICK_SEARCH_MODELS[providerName] || []) {
|
|
3573
|
+
addQuickSearchModel(rows, seen, providerName, model);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
const configuredSearch = normalizeSearchRouteConfig(pickerConfig.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
3577
|
+
if (configuredSearch?.provider && configuredSearch?.model) {
|
|
3578
|
+
addQuickSearchModel(rows, seen, configuredSearch.provider, {
|
|
3579
|
+
id: configuredSearch.model,
|
|
3580
|
+
display: configuredSearch.model,
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
const mainModel = currentMainSearchModelMeta();
|
|
3584
|
+
if (mainModel && searchCapableFor(route.provider, mainModel)) {
|
|
3585
|
+
addQuickSearchModel(rows, seen, route.provider, {
|
|
3586
|
+
id: route.model,
|
|
3587
|
+
display: route.model,
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3590
|
+
return searchModelsFromRows(rows);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
function searchModelsFromRows(rows) {
|
|
3594
|
+
return sortProviderModels((rows || [])
|
|
3595
|
+
.filter((row) => row.supportsWebSearch === true)
|
|
3596
|
+
.map((row) => ({
|
|
3597
|
+
...row,
|
|
3598
|
+
provider: normalizeSearchProviderId(row.provider),
|
|
3599
|
+
searchCapable: true,
|
|
3600
|
+
searchToolType: row.searchToolType || 'web_search',
|
|
3601
|
+
})));
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
function searchRowsWithDefault(rows = []) {
|
|
3605
|
+
const out = [];
|
|
3606
|
+
const seen = new Set();
|
|
3607
|
+
addDefaultSearchModel(out, seen);
|
|
3608
|
+
for (const row of rows || []) {
|
|
3609
|
+
const providerName = normalizeSearchProviderId(row?.provider);
|
|
3610
|
+
const modelId = clean(row?.id || row?.model);
|
|
3611
|
+
if (providerName === SEARCH_DEFAULT_PROVIDER && modelId.toLowerCase() === SEARCH_DEFAULT_MODEL) continue;
|
|
3612
|
+
const key = `${providerName}:${modelId}`;
|
|
3613
|
+
if (!providerName || !modelId || seen.has(key)) continue;
|
|
3614
|
+
seen.add(key);
|
|
3615
|
+
out.push(row);
|
|
3616
|
+
}
|
|
3617
|
+
return out;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
async function collectSearchProviderModels({ force = false } = {}) {
|
|
3621
|
+
if (!force && Array.isArray(searchProviderModelsCache.models)) {
|
|
3622
|
+
return providerModelsFromCacheRows(searchRowsWithDefault(searchProviderModelsCache.models));
|
|
3623
|
+
}
|
|
3624
|
+
if (!force && Array.isArray(providerModelsCache.models)) {
|
|
3625
|
+
const rows = searchRowsWithDefault(searchModelsFromRows(providerModelsCache.models));
|
|
3626
|
+
searchProviderModelsCache = { models: rows, at: Date.now() };
|
|
3627
|
+
return providerModelsFromCacheRows(rows);
|
|
3628
|
+
}
|
|
3629
|
+
if (!force) {
|
|
3630
|
+
const rows = searchRowsWithDefault(quickSearchProviderModelRows());
|
|
3631
|
+
searchProviderModelsCache = { models: rows, at: Date.now() };
|
|
3632
|
+
return providerModelsFromCacheRows(rows);
|
|
3633
|
+
}
|
|
3634
|
+
if (!searchProviderModelsPromise) {
|
|
3635
|
+
searchProviderModelsPromise = loadSearchProviderModelsFresh()
|
|
3636
|
+
.then((models) => {
|
|
3637
|
+
searchProviderModelsCache = { models, at: Date.now() };
|
|
3638
|
+
return models;
|
|
3639
|
+
})
|
|
3640
|
+
.finally(() => {
|
|
3641
|
+
searchProviderModelsPromise = null;
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
return providerModelsFromCacheRows(await searchProviderModelsPromise);
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
function enabledSearchProviderConfig() {
|
|
2398
3648
|
ensureFullConfig();
|
|
3649
|
+
const out = {};
|
|
3650
|
+
for (const [name, providerConfig] of Object.entries(config.providers || {})) {
|
|
3651
|
+
const providerName = normalizeSearchProviderId(name);
|
|
3652
|
+
if (!providerConfig?.enabled || !isSearchCapableProvider(providerName)) continue;
|
|
3653
|
+
out[providerName] = { ...providerConfig, enabled: true };
|
|
3654
|
+
}
|
|
3655
|
+
return out;
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
async function loadSearchProviderModelsFresh() {
|
|
3659
|
+
const searchProviders = enabledSearchProviderConfig();
|
|
3660
|
+
const providerNames = Object.keys(searchProviders);
|
|
3661
|
+
if (!providerNames.length) return [];
|
|
2399
3662
|
await ensureProvidersReady(config.providers || {});
|
|
2400
|
-
const
|
|
3663
|
+
const providerResults = await Promise.all(providerNames.map(async (name) => {
|
|
3664
|
+
const provider = reg.getProvider(name);
|
|
3665
|
+
if (typeof provider?.listModels !== 'function') return [];
|
|
3666
|
+
try {
|
|
3667
|
+
const models = await provider.listModels();
|
|
3668
|
+
if (!Array.isArray(models)) return [];
|
|
3669
|
+
const rows = [];
|
|
3670
|
+
for (const m of models) {
|
|
3671
|
+
if (!m?.id || !isSelectableLlmModel(m)) continue;
|
|
3672
|
+
const row = providerModelCacheRow(name, m);
|
|
3673
|
+
if (row.supportsWebSearch !== true) continue;
|
|
3674
|
+
rows.push({
|
|
3675
|
+
...row,
|
|
3676
|
+
provider: normalizeSearchProviderId(row.provider),
|
|
3677
|
+
searchCapable: true,
|
|
3678
|
+
searchToolType: row.searchToolType || 'web_search',
|
|
3679
|
+
});
|
|
3680
|
+
modelMetaByRoute.set(modelMetaKey(name, m.id), row);
|
|
3681
|
+
}
|
|
3682
|
+
return rows;
|
|
3683
|
+
} catch {
|
|
3684
|
+
// Keep the picker responsive if one search-capable provider has a
|
|
3685
|
+
// transient catalog/auth failure.
|
|
3686
|
+
return [];
|
|
3687
|
+
}
|
|
3688
|
+
}));
|
|
2401
3689
|
const results = [];
|
|
2402
3690
|
const seen = new Set();
|
|
2403
|
-
|
|
2404
|
-
|
|
3691
|
+
addDefaultSearchModel(results, seen);
|
|
3692
|
+
for (const row of providerResults.flat()) {
|
|
3693
|
+
const key = `${normalizeSearchProviderId(row.provider)}:${row.id}`;
|
|
3694
|
+
if (seen.has(key)) continue;
|
|
3695
|
+
seen.add(key);
|
|
3696
|
+
results.push(row);
|
|
3697
|
+
}
|
|
3698
|
+
return results;
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
async function loadProviderModelsFresh() {
|
|
3702
|
+
ensureFullConfig();
|
|
3703
|
+
await ensureProvidersReady(config.providers || {});
|
|
3704
|
+
const allProviders = [...reg.getAllProviders()];
|
|
3705
|
+
const providerResults = await Promise.all(allProviders.map(async ([name, provider]) => {
|
|
3706
|
+
if (typeof provider?.listModels !== 'function') return [];
|
|
2405
3707
|
try {
|
|
2406
3708
|
const models = await provider.listModels();
|
|
2407
|
-
if (Array.isArray(models))
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
seen.add(key);
|
|
2414
|
-
const row = providerModelCacheRow(name, m);
|
|
2415
|
-
results.push(row);
|
|
2416
|
-
modelMetaByRoute.set(modelMetaKey(name, m.id), row);
|
|
2417
|
-
}
|
|
3709
|
+
if (!Array.isArray(models)) return [];
|
|
3710
|
+
const rows = [];
|
|
3711
|
+
for (const m of models) {
|
|
3712
|
+
if (!m?.id) continue;
|
|
3713
|
+
if (!isSelectableLlmModel(m)) continue;
|
|
3714
|
+
rows.push(providerModelCacheRow(name, m));
|
|
2418
3715
|
}
|
|
3716
|
+
return rows;
|
|
2419
3717
|
} catch {
|
|
2420
3718
|
// Ignore per-provider catalog failures so one bad credential or
|
|
2421
3719
|
// transient /models error does not hide other authenticated models.
|
|
3720
|
+
return [];
|
|
2422
3721
|
}
|
|
3722
|
+
}));
|
|
3723
|
+
const results = [];
|
|
3724
|
+
const seen = new Set();
|
|
3725
|
+
for (const row of providerResults.flat()) {
|
|
3726
|
+
const key = `${row.provider}:${row.id}`;
|
|
3727
|
+
if (seen.has(key)) continue;
|
|
3728
|
+
seen.add(key);
|
|
3729
|
+
results.push(row);
|
|
3730
|
+
modelMetaByRoute.set(modelMetaKey(row.provider, row.id), row);
|
|
2423
3731
|
}
|
|
2424
3732
|
return results;
|
|
2425
3733
|
}
|
|
2426
3734
|
|
|
2427
|
-
async function collectProviderModels({ force = false } = {}) {
|
|
3735
|
+
async function collectProviderModels({ force = false, quick = false } = {}) {
|
|
2428
3736
|
if (!force && Array.isArray(providerModelsCache.models)) {
|
|
2429
3737
|
return providerModelsFromCacheRows(providerModelsCache.models);
|
|
2430
3738
|
}
|
|
3739
|
+
if (!force && quick) {
|
|
3740
|
+
warmProviderModelCache();
|
|
3741
|
+
return quickProviderModelRows();
|
|
3742
|
+
}
|
|
2431
3743
|
if (!providerModelsPromise) {
|
|
2432
3744
|
providerModelsPromise = loadProviderModelsFresh()
|
|
2433
3745
|
.then((models) => {
|
|
@@ -2465,7 +3777,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2465
3777
|
const models = await collectProviderModels();
|
|
2466
3778
|
const picked = models[0] || null;
|
|
2467
3779
|
if (!picked) {
|
|
2468
|
-
throw new Error('No provider models available.
|
|
3780
|
+
throw new Error('No provider models available. Open /providers to sign in, then /model to choose a model.');
|
|
2469
3781
|
}
|
|
2470
3782
|
route = {
|
|
2471
3783
|
...route,
|
|
@@ -2498,7 +3810,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2498
3810
|
|
|
2499
3811
|
function requireModelRoute() {
|
|
2500
3812
|
if (routeHasModel()) return;
|
|
2501
|
-
throw new Error('No model configured.
|
|
3813
|
+
throw new Error('No model configured. Open /providers to sign in, then /model to choose a model.');
|
|
2502
3814
|
}
|
|
2503
3815
|
|
|
2504
3816
|
async function recreateCurrentSessionIfReady() {
|
|
@@ -2537,8 +3849,8 @@ function parsedProviderModelVersion(id) {
|
|
|
2537
3849
|
const coreMemoryContext = await loadCoreMemoryContext();
|
|
2538
3850
|
if (closeRequested) throw new Error('runtime is closing');
|
|
2539
3851
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
3852
|
+
const workflow = activeWorkflowSummary(config, dataDir);
|
|
2540
3853
|
const workflowContext = workflowContextBlock(config, dataDir);
|
|
2541
|
-
const workspaceContext = buildWorkspaceContext();
|
|
2542
3854
|
const sessionOpts = {
|
|
2543
3855
|
provider: route.provider,
|
|
2544
3856
|
model: route.model,
|
|
@@ -2553,9 +3865,12 @@ function parsedProviderModelVersion(id) {
|
|
|
2553
3865
|
disallowedTools: LEAD_DISALLOWED_TOOLS,
|
|
2554
3866
|
cwd: currentCwd,
|
|
2555
3867
|
coreMemoryContext,
|
|
3868
|
+
workflow,
|
|
2556
3869
|
workflowContext,
|
|
2557
|
-
workspaceContext,
|
|
2558
3870
|
fast: route.fast === true,
|
|
3871
|
+
compaction: config.compaction && typeof config.compaction === 'object'
|
|
3872
|
+
? normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() })
|
|
3873
|
+
: undefined,
|
|
2559
3874
|
};
|
|
2560
3875
|
if (hasOwn(route, 'effort') || route.effectiveEffort) {
|
|
2561
3876
|
sessionOpts.effort = route.effectiveEffort || null;
|
|
@@ -2563,15 +3878,55 @@ function parsedProviderModelVersion(id) {
|
|
|
2563
3878
|
session = mgr.createSession(sessionOpts);
|
|
2564
3879
|
sessionNeedsCwdRefresh = false;
|
|
2565
3880
|
Object.defineProperty(session, 'beforeToolHook', {
|
|
2566
|
-
value: (input) => hooks.beforeTool(
|
|
3881
|
+
value: (input) => hooks.beforeTool(hookCommonPayload({
|
|
3882
|
+
...input,
|
|
3883
|
+
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
3884
|
+
tool_name: input?.name || input?.tool_name,
|
|
3885
|
+
tool_input: input?.args || input?.tool_input,
|
|
3886
|
+
tool_use_id: input?.toolCallId || input?.tool_use_id,
|
|
3887
|
+
cwd: input?.cwd || currentCwd,
|
|
3888
|
+
})),
|
|
3889
|
+
enumerable: false,
|
|
3890
|
+
configurable: true,
|
|
3891
|
+
writable: true,
|
|
3892
|
+
});
|
|
3893
|
+
// PostToolUse: bridge runtime tool completions to the standard hook bus.
|
|
3894
|
+
// dispatch() returns a promise; the loop's afterToolHook caller already
|
|
3895
|
+
// try/catches, so a rejection cannot escape the tool loop.
|
|
3896
|
+
Object.defineProperty(session, 'afterToolHook', {
|
|
3897
|
+
value: (input) => hooks.dispatch('PostToolUse', hookCommonPayload({
|
|
3898
|
+
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
3899
|
+
cwd: input?.cwd || currentCwd,
|
|
3900
|
+
tool_name: input?.name,
|
|
3901
|
+
tool_input: input?.args,
|
|
3902
|
+
tool_use_id: input?.toolCallId || input?.tool_use_id,
|
|
3903
|
+
tool_response: input?.result,
|
|
3904
|
+
})),
|
|
2567
3905
|
enumerable: false,
|
|
2568
3906
|
configurable: true,
|
|
2569
3907
|
writable: true,
|
|
2570
3908
|
});
|
|
2571
|
-
applyDeferredToolSurface(session, mode, standaloneTools);
|
|
3909
|
+
applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
2572
3910
|
applyPreSessionToolSelection();
|
|
2573
|
-
statusRoutes
|
|
3911
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
2574
3912
|
hooks.emit('session:create', { sessionId: session.id, provider: route.provider, model: route.model, toolMode: mode, cwd: currentCwd });
|
|
3913
|
+
// SessionStart: bridge to the standard Claude Code hook bus. Best-effort;
|
|
3914
|
+
// a hook error must never break session creation. additionalContext is
|
|
3915
|
+
// injected before the first user turn as a system-reminder context pair.
|
|
3916
|
+
try {
|
|
3917
|
+
const startSource = /resume/i.test(String(reason || ''))
|
|
3918
|
+
? 'resume'
|
|
3919
|
+
: (/clear/i.test(String(reason || '')) ? 'clear' : 'startup');
|
|
3920
|
+
const startDispatch = await hooks.dispatch('SessionStart', hookCommonPayload({ session_id: session.id, source: startSource, model: route.model }));
|
|
3921
|
+
const startContext = Array.isArray(startDispatch?.additionalContext)
|
|
3922
|
+
? startDispatch.additionalContext.join('\n\n')
|
|
3923
|
+
: String(startDispatch?.additionalContext || '');
|
|
3924
|
+
if (startContext.trim()) {
|
|
3925
|
+
session.messages.push({ role: 'user', content: `<system-reminder>\n# SessionStart Hook Context\n${startContext.trim()}\n</system-reminder>` });
|
|
3926
|
+
session.messages.push({ role: 'assistant', content: '.' });
|
|
3927
|
+
session.updatedAt = Date.now();
|
|
3928
|
+
}
|
|
3929
|
+
} catch { /* best-effort: never break session create */ }
|
|
2575
3930
|
bootProfile('session:create:ready', {
|
|
2576
3931
|
ms: (performance.now() - startedAt).toFixed(1),
|
|
2577
3932
|
reason,
|
|
@@ -2590,7 +3945,10 @@ function parsedProviderModelVersion(id) {
|
|
|
2590
3945
|
}
|
|
2591
3946
|
|
|
2592
3947
|
function scheduleLeadSessionPrewarm() {
|
|
2593
|
-
if (
|
|
3948
|
+
if (!sessionPrewarmEnabled) {
|
|
3949
|
+
bootProfile('session:prewarm-skipped');
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
2594
3952
|
const timer = setTimeout(() => {
|
|
2595
3953
|
if (closeRequested || session?.id || sessionCreatePromise || activeTurnCount > 0) return;
|
|
2596
3954
|
void createCurrentSession('prewarm')
|
|
@@ -2601,7 +3959,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2601
3959
|
}
|
|
2602
3960
|
|
|
2603
3961
|
function scheduleProviderWarmup(delayMs = providerWarmupDelayMs) {
|
|
2604
|
-
if (
|
|
3962
|
+
if (!providerWarmupEnabled) {
|
|
2605
3963
|
bootProfile('providers:warm-skipped');
|
|
2606
3964
|
return;
|
|
2607
3965
|
}
|
|
@@ -2609,6 +3967,10 @@ function parsedProviderModelVersion(id) {
|
|
|
2609
3967
|
providerWarmupTimer = setTimeout(() => {
|
|
2610
3968
|
providerWarmupTimer = null;
|
|
2611
3969
|
if (closeRequested) return;
|
|
3970
|
+
if (!firstTurnCompleted && !envFlag('MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN')) {
|
|
3971
|
+
bootProfile('providers:warm-deferred', { reason: 'first-turn-pending' });
|
|
3972
|
+
return;
|
|
3973
|
+
}
|
|
2612
3974
|
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
2613
3975
|
bootProfile('providers:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
2614
3976
|
scheduleProviderWarmup(backgroundBusyRetryMs);
|
|
@@ -2616,8 +3978,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2616
3978
|
}
|
|
2617
3979
|
const providersStartedAt = performance.now();
|
|
2618
3980
|
try {
|
|
2619
|
-
|
|
2620
|
-
configHasSecrets = true;
|
|
3981
|
+
reloadFullConfig();
|
|
2621
3982
|
} catch (error) {
|
|
2622
3983
|
bootProfile('config:full-failed', { error: error?.message || String(error) });
|
|
2623
3984
|
}
|
|
@@ -2625,28 +3986,27 @@ function parsedProviderModelVersion(id) {
|
|
|
2625
3986
|
.then(() => {
|
|
2626
3987
|
bootProfile('providers:init:ready', { ms: (performance.now() - providersStartedAt).toFixed(1) });
|
|
2627
3988
|
if (closeRequested) return null;
|
|
2628
|
-
|
|
2629
|
-
bootProfile('providers:optional-warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
2630
|
-
scheduleProviderWarmup(backgroundBusyRetryMs);
|
|
2631
|
-
return null;
|
|
2632
|
-
}
|
|
2633
|
-
return cachedProviderSetup();
|
|
2634
|
-
})
|
|
2635
|
-
.then((setup) => {
|
|
2636
|
-
if (!setup) return;
|
|
2637
|
-
bootProfile('provider-setup:warm-ready', {
|
|
2638
|
-
api: Array.isArray(setup?.api) ? setup.api.length : 0,
|
|
2639
|
-
oauth: Array.isArray(setup?.oauth) ? setup.oauth.length : 0,
|
|
2640
|
-
local: Array.isArray(setup?.local) ? setup.local.length : 0,
|
|
2641
|
-
});
|
|
3989
|
+
return true;
|
|
2642
3990
|
})
|
|
2643
3991
|
.catch((error) => bootProfile('providers:warm-failed', { error: error?.message || String(error) }));
|
|
2644
3992
|
}, delayMs);
|
|
2645
3993
|
providerWarmupTimer.unref?.();
|
|
2646
3994
|
}
|
|
2647
3995
|
|
|
3996
|
+
function scheduleProviderSetupWarmup(delayMs = providerSetupWarmupDelayMs) {
|
|
3997
|
+
if (providerSetupWarmupTimer || closeRequested) return;
|
|
3998
|
+
providerSetupWarmupTimer = setTimeout(() => {
|
|
3999
|
+
providerSetupWarmupTimer = null;
|
|
4000
|
+
if (closeRequested) return;
|
|
4001
|
+
void cachedProviderSetup()
|
|
4002
|
+
.then(() => bootProfile('provider-setup:warm-ready'))
|
|
4003
|
+
.catch((error) => bootProfile('provider-setup:warm-failed', { error: error?.message || String(error) }));
|
|
4004
|
+
}, delayMs);
|
|
4005
|
+
providerSetupWarmupTimer.unref?.();
|
|
4006
|
+
}
|
|
4007
|
+
|
|
2648
4008
|
function scheduleProviderModelWarmup(delayMs = providerModelWarmupDelayMs) {
|
|
2649
|
-
if (
|
|
4009
|
+
if (!modelPrefetchEnabled) return;
|
|
2650
4010
|
if (providerModelWarmupTimer || closeRequested) return;
|
|
2651
4011
|
providerModelWarmupTimer = setTimeout(() => {
|
|
2652
4012
|
providerModelWarmupTimer = null;
|
|
@@ -2661,11 +4021,75 @@ function parsedProviderModelVersion(id) {
|
|
|
2661
4021
|
providerModelWarmupTimer.unref?.();
|
|
2662
4022
|
}
|
|
2663
4023
|
|
|
4024
|
+
function scheduleStatuslineUsageWarmup(delayMs = statuslineUsageWarmupDelayMs) {
|
|
4025
|
+
const providerId = clean(route?.provider);
|
|
4026
|
+
if (!providerId || !providerId.includes('oauth')) {
|
|
4027
|
+
bootProfile('statusline-usage:warm-skipped', { provider: providerId || null });
|
|
4028
|
+
return;
|
|
4029
|
+
}
|
|
4030
|
+
if (statuslineUsageWarmupTimer || closeRequested) return;
|
|
4031
|
+
statuslineUsageWarmupTimer = setTimeout(async () => {
|
|
4032
|
+
statuslineUsageWarmupTimer = null;
|
|
4033
|
+
if (closeRequested) return;
|
|
4034
|
+
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
4035
|
+
bootProfile('statusline-usage:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
4036
|
+
scheduleStatuslineUsageWarmup(backgroundBusyRetryMs);
|
|
4037
|
+
return;
|
|
4038
|
+
}
|
|
4039
|
+
try {
|
|
4040
|
+
ensureConfigForRouteProvider();
|
|
4041
|
+
await ensureProvidersReady(ensureProviderEnabled(config, route.provider));
|
|
4042
|
+
if (closeRequested) return;
|
|
4043
|
+
refreshStatuslineUsageSnapshot(route);
|
|
4044
|
+
bootProfile('statusline-usage:warm-ready', { provider: clean(route?.provider) });
|
|
4045
|
+
} catch (error) {
|
|
4046
|
+
bootProfile('statusline-usage:warm-failed', { error: error?.message || String(error) });
|
|
4047
|
+
} finally {
|
|
4048
|
+
scheduleStatuslineUsageRefresh();
|
|
4049
|
+
}
|
|
4050
|
+
}, delayMs);
|
|
4051
|
+
statuslineUsageWarmupTimer.unref?.();
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
// Idle keep-alive loop: periodically re-fetch the OAuth usage snapshot so its
|
|
4055
|
+
// cachedAt stays "live-fresh" and the statusline usage segment does not vanish
|
|
4056
|
+
// after LIVE_USAGE_SNAPSHOT_MAX_AGE_MS while the session is idle. Turn-driven
|
|
4057
|
+
// refreshes (recordStandaloneStatusTelemetry) already cover active sessions.
|
|
4058
|
+
function scheduleStatuslineUsageRefresh(delayMs = statuslineUsageRefreshDelayMs) {
|
|
4059
|
+
const providerId = clean(route?.provider);
|
|
4060
|
+
if (!providerId || !providerId.includes('oauth')) return;
|
|
4061
|
+
if (statuslineUsageRefreshTimer || closeRequested) return;
|
|
4062
|
+
statuslineUsageRefreshTimer = setTimeout(async () => {
|
|
4063
|
+
statuslineUsageRefreshTimer = null;
|
|
4064
|
+
if (closeRequested) return;
|
|
4065
|
+
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
4066
|
+
// Active turns refresh usage on their own; just re-arm the idle loop.
|
|
4067
|
+
scheduleStatuslineUsageRefresh();
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
try {
|
|
4071
|
+
ensureConfigForRouteProvider();
|
|
4072
|
+
await ensureProvidersReady(ensureProviderEnabled(config, route.provider));
|
|
4073
|
+
if (closeRequested) return;
|
|
4074
|
+
refreshStatuslineUsageSnapshot(route);
|
|
4075
|
+
} catch {
|
|
4076
|
+
// Usage display must never affect the session runtime.
|
|
4077
|
+
} finally {
|
|
4078
|
+
scheduleStatuslineUsageRefresh();
|
|
4079
|
+
}
|
|
4080
|
+
}, delayMs);
|
|
4081
|
+
statuslineUsageRefreshTimer.unref?.();
|
|
4082
|
+
}
|
|
4083
|
+
|
|
2664
4084
|
function scheduleChannelStart(delayMs = channelStartDelayMs) {
|
|
2665
4085
|
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
2666
4086
|
bootProfile('channels:start-skipped');
|
|
2667
4087
|
return;
|
|
2668
4088
|
}
|
|
4089
|
+
if (!channelsEnabled()) {
|
|
4090
|
+
bootProfile('channels:start-disabled');
|
|
4091
|
+
return;
|
|
4092
|
+
}
|
|
2669
4093
|
if (channelStartTimer || closeRequested) return;
|
|
2670
4094
|
bootProfile('channels:start-scheduled', { delayMs });
|
|
2671
4095
|
channelStartTimer = setTimeout(() => {
|
|
@@ -2700,12 +4124,31 @@ function parsedProviderModelVersion(id) {
|
|
|
2700
4124
|
});
|
|
2701
4125
|
}
|
|
2702
4126
|
|
|
2703
|
-
bootProfile('session-runtime:ready', {
|
|
4127
|
+
bootProfile('session-runtime:ready', {
|
|
4128
|
+
lazySession: true,
|
|
4129
|
+
prewarmSession: sessionPrewarmEnabled,
|
|
4130
|
+
providerWarmup: providerWarmupEnabled,
|
|
4131
|
+
codeGraphPrewarm: codeGraphPrewarmEnabled,
|
|
4132
|
+
});
|
|
2704
4133
|
scheduleLeadSessionPrewarm();
|
|
4134
|
+
// Lazy mode (default): skip the startup prewarm entirely; the first turn
|
|
4135
|
+
// triggers it instead (see ask()). Eager mode keeps the old startup schedule.
|
|
4136
|
+
if (!codeGraphPrewarmLazy) {
|
|
4137
|
+
scheduleCodeGraphPrewarm(codeGraphPrewarmDelayMs, 'startup');
|
|
4138
|
+
} else {
|
|
4139
|
+
bootProfile('code-graph:prewarm-lazy', { reason: 'startup-deferred-to-first-turn' });
|
|
4140
|
+
}
|
|
4141
|
+
scheduleProviderSetupWarmup();
|
|
2705
4142
|
scheduleProviderWarmup();
|
|
4143
|
+
// Warm the provider model catalog in the background, but keep it on its own
|
|
4144
|
+
// delay so short-lived detached runtimes can exit before /models I/O starts.
|
|
4145
|
+
// Operators that want earlier catalog warming can lower
|
|
4146
|
+
// MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS explicitly.
|
|
4147
|
+
scheduleProviderModelWarmup();
|
|
4148
|
+
scheduleStatuslineUsageWarmup();
|
|
2706
4149
|
scheduleChannelStart();
|
|
2707
4150
|
|
|
2708
|
-
function contextStatusCacheKeyFor({ messages, tools
|
|
4151
|
+
function contextStatusCacheKeyFor({ messages, tools }) {
|
|
2709
4152
|
const compaction = session?.compaction || {};
|
|
2710
4153
|
const lastMessage = messages[messages.length - 1] || null;
|
|
2711
4154
|
return {
|
|
@@ -2715,7 +4158,6 @@ function parsedProviderModelVersion(id) {
|
|
|
2715
4158
|
model: route.model,
|
|
2716
4159
|
cwd: currentCwd,
|
|
2717
4160
|
mode,
|
|
2718
|
-
bridgeMode,
|
|
2719
4161
|
messages,
|
|
2720
4162
|
messageCount: messages.length,
|
|
2721
4163
|
lastMessage,
|
|
@@ -2788,23 +4230,25 @@ function parsedProviderModelVersion(id) {
|
|
|
2788
4230
|
get toolMode() {
|
|
2789
4231
|
return mode;
|
|
2790
4232
|
},
|
|
2791
|
-
get bridgeMode() {
|
|
2792
|
-
return bridge.getDefaultMode();
|
|
2793
|
-
},
|
|
2794
4233
|
get autoClear() {
|
|
2795
4234
|
return normalizeAutoClearConfig(config.autoClear);
|
|
2796
4235
|
},
|
|
2797
4236
|
get systemShell() {
|
|
2798
4237
|
return normalizeSystemShellConfig(config.shell);
|
|
2799
4238
|
},
|
|
4239
|
+
get searchRoute() {
|
|
4240
|
+
searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
4241
|
+
return searchRoute;
|
|
4242
|
+
},
|
|
2800
4243
|
get workflow() {
|
|
2801
4244
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
2802
|
-
|
|
2803
|
-
|
|
4245
|
+
if (session?.workflow && typeof session.workflow === 'object') {
|
|
4246
|
+
return workflowSummary(session.workflow);
|
|
4247
|
+
}
|
|
4248
|
+
return activeWorkflowSummary(config, dataDir);
|
|
2804
4249
|
},
|
|
2805
4250
|
get outputStyle() {
|
|
2806
|
-
|
|
2807
|
-
return outputStyleStatus(dataDir).current;
|
|
4251
|
+
return getOutputStyleStatusCached().current;
|
|
2808
4252
|
},
|
|
2809
4253
|
get cwd() {
|
|
2810
4254
|
return currentCwd;
|
|
@@ -2813,10 +4257,16 @@ function parsedProviderModelVersion(id) {
|
|
|
2813
4257
|
return session;
|
|
2814
4258
|
},
|
|
2815
4259
|
contextStatus() {
|
|
2816
|
-
|
|
4260
|
+
// Prefer the in-flight working transcript while a turn is running so the
|
|
4261
|
+
// context gauge reflects LIVE growth (user turn + tool calls/results) as
|
|
4262
|
+
// it accumulates, instead of freezing at the pre-turn committed snapshot.
|
|
4263
|
+
// askSession() sets session.liveTurnMessages for the turn duration and
|
|
4264
|
+
// clears it on commit/cancel/error, after which we fall back to the
|
|
4265
|
+
// authoritative committed transcript.
|
|
4266
|
+
const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
|
|
4267
|
+
const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
|
|
2817
4268
|
const tools = Array.isArray(session?.tools) ? session.tools : [];
|
|
2818
|
-
const
|
|
2819
|
-
const cacheKey = contextStatusCacheKeyFor({ messages, tools, bridgeMode });
|
|
4269
|
+
const cacheKey = contextStatusCacheKeyFor({ messages, tools });
|
|
2820
4270
|
if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
|
|
2821
4271
|
return contextStatusCacheValue;
|
|
2822
4272
|
}
|
|
@@ -2829,7 +4279,9 @@ function parsedProviderModelVersion(id) {
|
|
|
2829
4279
|
const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
|
|
2830
4280
|
const effectiveWindow = Number(session?.contextWindow || rawWindow || 0);
|
|
2831
4281
|
const lastContextTokens = Number(session?.lastContextTokens || 0);
|
|
2832
|
-
const estimatedContextTokens =
|
|
4282
|
+
const estimatedContextTokens = estimateTranscriptContextUsage(messages, tools, {
|
|
4283
|
+
messageCount: messageSummary.count,
|
|
4284
|
+
});
|
|
2833
4285
|
const compactAt = Number(session?.compaction?.lastChangedAt || session?.compaction?.lastCompactAt || 0);
|
|
2834
4286
|
const usageAt = Number(session?.lastContextTokensUpdatedAt || 0);
|
|
2835
4287
|
const lastUsageStale = !!lastContextTokens && (
|
|
@@ -2839,31 +4291,58 @@ function parsedProviderModelVersion(id) {
|
|
|
2839
4291
|
);
|
|
2840
4292
|
const compactBoundaryTokens = Number(session?.compactBoundaryTokens || session?.compaction?.boundaryTokens || 0);
|
|
2841
4293
|
const displayWindow = compactBoundaryTokens || effectiveWindow;
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
4294
|
+
// The transcript estimate is the single source of truth for the displayed
|
|
4295
|
+
// context footprint. Provider-reported input_tokens (lastContextTokens)
|
|
4296
|
+
// swing non-monotonically and are not window-bounded on some providers
|
|
4297
|
+
// (e.g. OpenAI gpt-5.5 Responses API), so they are kept only as secondary
|
|
4298
|
+
// metadata (lastApiRequestTokens / usage.lastContextTokens) and never feed
|
|
4299
|
+
// the gauge numerator.
|
|
4300
|
+
const usedTokens = estimatedContextTokens;
|
|
2845
4301
|
const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
|
|
2846
4302
|
const autoCompactTokenLimit = Number(session?.autoCompactTokenLimit || 0);
|
|
2847
|
-
|
|
2848
|
-
|
|
4303
|
+
// The runtime fires auto-compaction at the effective boundary by default.
|
|
4304
|
+
// Compute the same trigger here so /context shows what will actually fire
|
|
4305
|
+
// before any compaction telemetry (session.compaction.triggerTokens) is
|
|
4306
|
+
// recorded. Explicit buffer settings can still lower the trigger.
|
|
4307
|
+
// An explicit autoCompactTokenLimit (provider/catalog/seed supplied)
|
|
4308
|
+
// still takes precedence and is honored verbatim when it sits at or below
|
|
4309
|
+
// the boundary.
|
|
4310
|
+
const defaultCompactBufferTokens = contextDefaultBufferTokens(compactBoundaryTokens, session?.compaction || {});
|
|
4311
|
+
const defaultCompactTriggerTokens = compactBoundaryTokens
|
|
4312
|
+
? Math.max(1, compactBoundaryTokens - defaultCompactBufferTokens)
|
|
4313
|
+
: 0;
|
|
4314
|
+
// Only an explicit auto-compact limit STRICTLY BELOW the boundary is a
|
|
4315
|
+
// real trigger; a persisted value == boundary is a legacy derived
|
|
4316
|
+
// full-window artifact and must fall through to the default trigger.
|
|
4317
|
+
// Likewise ignore persisted default-buffer telemetry so the displayed
|
|
4318
|
+
// trigger matches what the runtime now actually fires.
|
|
4319
|
+
const persistedTriggerTokens = Number(session?.compaction?.triggerTokens || 0);
|
|
4320
|
+
const legacyDefaultTriggerTelemetry = isLegacyDefaultContextBufferTelemetry(session?.compaction || {}, compactBoundaryTokens)
|
|
4321
|
+
&& persistedTriggerTokens > 0
|
|
4322
|
+
&& persistedTriggerTokens < compactBoundaryTokens;
|
|
4323
|
+
const usablePersistedTrigger = persistedTriggerTokens > 0
|
|
4324
|
+
&& !legacyDefaultTriggerTelemetry
|
|
4325
|
+
&& (!compactBoundaryTokens || persistedTriggerTokens < compactBoundaryTokens)
|
|
4326
|
+
? persistedTriggerTokens
|
|
4327
|
+
: 0;
|
|
4328
|
+
const compactTriggerTokens = autoCompactTokenLimit && compactBoundaryTokens && autoCompactTokenLimit < compactBoundaryTokens
|
|
2849
4329
|
? autoCompactTokenLimit
|
|
2850
|
-
:
|
|
2851
|
-
const compactBufferTokens = Number(
|
|
4330
|
+
: (usablePersistedTrigger || defaultCompactTriggerTokens || 0);
|
|
4331
|
+
const compactBufferTokens = Number(compactBoundaryTokens && compactTriggerTokens
|
|
4332
|
+
? Math.max(0, compactBoundaryTokens - compactTriggerTokens)
|
|
4333
|
+
: defaultCompactBufferTokens);
|
|
2852
4334
|
const value = {
|
|
2853
4335
|
sessionId: session?.id || null,
|
|
2854
4336
|
provider: route.provider,
|
|
2855
4337
|
model: route.model,
|
|
2856
4338
|
cwd: currentCwd,
|
|
2857
4339
|
toolMode: mode,
|
|
2858
|
-
bridgeMode,
|
|
2859
4340
|
contextWindow: displayWindow || effectiveWindow || null,
|
|
2860
4341
|
effectiveContextWindow: effectiveWindow || null,
|
|
2861
4342
|
rawContextWindow: rawWindow || null,
|
|
2862
4343
|
effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
|
|
2863
4344
|
usedTokens,
|
|
2864
|
-
usedSource:
|
|
2865
|
-
? 'last_api_request'
|
|
2866
|
-
: 'estimated',
|
|
4345
|
+
usedSource: 'estimated',
|
|
2867
4346
|
currentEstimatedTokens: estimatedContextTokens,
|
|
2868
4347
|
lastApiRequestTokens: lastContextTokens || 0,
|
|
2869
4348
|
lastApiRequestStale: lastUsageStale,
|
|
@@ -2901,26 +4380,64 @@ function parsedProviderModelVersion(id) {
|
|
|
2901
4380
|
return value;
|
|
2902
4381
|
},
|
|
2903
4382
|
listProviders() {
|
|
2904
|
-
return renderProviderStatus(
|
|
4383
|
+
return renderProviderStatus(displayConfig());
|
|
2905
4384
|
},
|
|
2906
4385
|
async getProviderSetup() {
|
|
2907
4386
|
return await cachedProviderSetup();
|
|
2908
4387
|
},
|
|
2909
4388
|
async getUsageDashboard(options = {}) {
|
|
2910
|
-
const
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
4389
|
+
const forceSetup = options?.force === true || options?.refresh === true;
|
|
4390
|
+
if (!forceSetup && usageDashboardCache.dashboard) {
|
|
4391
|
+
const cached = {
|
|
4392
|
+
...usageDashboardCache.dashboard,
|
|
4393
|
+
refresh: false,
|
|
4394
|
+
checking: false,
|
|
4395
|
+
cached: true,
|
|
4396
|
+
cachedAt: usageDashboardCache.at,
|
|
4397
|
+
};
|
|
4398
|
+
if (typeof options?.onUpdate === 'function') {
|
|
4399
|
+
try { options.onUpdate(cached); } catch {}
|
|
4400
|
+
}
|
|
4401
|
+
return cached;
|
|
4402
|
+
}
|
|
4403
|
+
if (!forceSetup && usageDashboardPromise) return await usageDashboardPromise;
|
|
4404
|
+
const quickSetup = options?.quickSetup !== false;
|
|
4405
|
+
const getProvider = (providerId) => reg.getProvider(providerId);
|
|
4406
|
+
const log = (message) => {
|
|
4407
|
+
if (process.env.MIXDOG_USAGE_TRACE) {
|
|
4408
|
+
try { process.stderr.write(`[usage] ${message}\n`); } catch {}
|
|
4409
|
+
}
|
|
4410
|
+
};
|
|
4411
|
+
if (quickSetup && typeof options?.onUpdate === 'function') {
|
|
4412
|
+
const previewConfig = displayConfig();
|
|
4413
|
+
const previewSetup = await cachedProviderSetup({ force: false, quick: true });
|
|
4414
|
+
await createUsageDashboard(previewConfig, {
|
|
4415
|
+
...(options || {}),
|
|
4416
|
+
preview: true,
|
|
4417
|
+
setup: previewSetup,
|
|
4418
|
+
getProvider,
|
|
4419
|
+
log,
|
|
4420
|
+
});
|
|
4421
|
+
}
|
|
4422
|
+
const buildDashboard = async () => {
|
|
4423
|
+
const dashboard = await createUsageDashboard(displayConfig(), {
|
|
4424
|
+
...(options || {}),
|
|
4425
|
+
setup: await cachedProviderSetup({ force: forceSetup, quick: false }),
|
|
4426
|
+
getProvider,
|
|
4427
|
+
log,
|
|
4428
|
+
});
|
|
4429
|
+
usageDashboardCache = { dashboard, at: Date.now() };
|
|
4430
|
+
return dashboard;
|
|
4431
|
+
};
|
|
4432
|
+
if (forceSetup) return await buildDashboard();
|
|
4433
|
+
usageDashboardPromise = buildDashboard()
|
|
4434
|
+
.finally(() => {
|
|
4435
|
+
usageDashboardPromise = null;
|
|
4436
|
+
});
|
|
4437
|
+
return await usageDashboardPromise;
|
|
2921
4438
|
},
|
|
2922
4439
|
getOnboardingStatus() {
|
|
2923
|
-
const nextConfig =
|
|
4440
|
+
const nextConfig = displayConfig();
|
|
2924
4441
|
return {
|
|
2925
4442
|
completed: nextConfig?.onboarding?.completed === true,
|
|
2926
4443
|
version: nextConfig?.onboarding?.version || 0,
|
|
@@ -2931,17 +4448,118 @@ function parsedProviderModelVersion(id) {
|
|
|
2931
4448
|
getAutoClear() {
|
|
2932
4449
|
return normalizeAutoClearConfig(config.autoClear);
|
|
2933
4450
|
},
|
|
4451
|
+
// --- User profile (/profile statusline command) ---------------------
|
|
4452
|
+
// getProfile returns the normalized { title, language } plus the resolved
|
|
4453
|
+
// language catalog entry and the full language list for the picker UI.
|
|
4454
|
+
getProfile() {
|
|
4455
|
+
const profile = cfgMod.normalizeProfileConfig(config.profile);
|
|
4456
|
+
return {
|
|
4457
|
+
...profile,
|
|
4458
|
+
languageEntry: cfgMod.profileLanguageEntry(profile.language),
|
|
4459
|
+
languages: cfgMod.PROFILE_LANGUAGES,
|
|
4460
|
+
};
|
|
4461
|
+
},
|
|
4462
|
+
// setProfile patches title and/or language and persists. Unknown language
|
|
4463
|
+
// ids normalize back to 'system'. Prompt-side injection is wired separately
|
|
4464
|
+
// (composeSystemPrompt) — this only owns the stored value.
|
|
4465
|
+
setProfile(input = {}) {
|
|
4466
|
+
const current = cfgMod.normalizeProfileConfig(config.profile);
|
|
4467
|
+
const next = { ...current };
|
|
4468
|
+
if (hasOwn(input, 'title') || hasOwn(input, 'name')) {
|
|
4469
|
+
next.title = input.title ?? input.name ?? '';
|
|
4470
|
+
}
|
|
4471
|
+
if (hasOwn(input, 'language') || hasOwn(input, 'lang')) {
|
|
4472
|
+
next.language = input.language ?? input.lang ?? 'system';
|
|
4473
|
+
}
|
|
4474
|
+
const normalized = cfgMod.normalizeProfileConfig(next);
|
|
4475
|
+
saveConfigAndAdopt({ ...config, profile: normalized });
|
|
4476
|
+
return this.getProfile();
|
|
4477
|
+
},
|
|
4478
|
+
getCompactionSettings() {
|
|
4479
|
+
return normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
|
|
4480
|
+
},
|
|
4481
|
+
setCompactionSettings(input = {}) {
|
|
4482
|
+
const current = normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
|
|
4483
|
+
const next = { ...current };
|
|
4484
|
+
if (hasOwn(input, 'auto')) next.auto = input.auto !== false;
|
|
4485
|
+
if (hasOwn(input, 'enabled')) next.auto = input.enabled !== false;
|
|
4486
|
+
if (hasOwn(input, 'type') || hasOwn(input, 'compactType') || hasOwn(input, 'compact_type')) {
|
|
4487
|
+
const requestedType = input.type ?? input.compactType ?? input.compact_type;
|
|
4488
|
+
const compactType = normalizeCompactTypeSetting(requestedType, current.compactType || current.type || 'semantic');
|
|
4489
|
+
if (compactType === 'recall-fasttrack' && !memoryEnabled()) {
|
|
4490
|
+
throw new Error('recall-fasttrack compact requires memory to be enabled');
|
|
4491
|
+
}
|
|
4492
|
+
next.type = compactType;
|
|
4493
|
+
next.compactType = compactType;
|
|
4494
|
+
}
|
|
4495
|
+
const nextConfig = { ...config };
|
|
4496
|
+
nextConfig.compaction = normalizeCompactionConfig(next, { memoryEnabled: memoryEnabled() });
|
|
4497
|
+
saveConfigAndAdopt(nextConfig);
|
|
4498
|
+
if (session) {
|
|
4499
|
+
session.compaction = {
|
|
4500
|
+
...(session.compaction || {}),
|
|
4501
|
+
...normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() }),
|
|
4502
|
+
};
|
|
4503
|
+
}
|
|
4504
|
+
invalidateContextStatusCache();
|
|
4505
|
+
return normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
|
|
4506
|
+
},
|
|
4507
|
+
getMemorySettings() {
|
|
4508
|
+
return {
|
|
4509
|
+
enabled: memoryEnabled(),
|
|
4510
|
+
compactFastTrackAvailable: memoryEnabled(),
|
|
4511
|
+
};
|
|
4512
|
+
},
|
|
4513
|
+
async setMemoryEnabled(enabled) {
|
|
4514
|
+
const nextConfig = setModuleEnabledInConfig({ ...config }, 'memory', enabled !== false);
|
|
4515
|
+
if (enabled === false) {
|
|
4516
|
+
nextConfig.compaction = normalizeCompactionConfig(nextConfig.compaction, { memoryEnabled: false });
|
|
4517
|
+
}
|
|
4518
|
+
saveConfigAndAdopt(nextConfig);
|
|
4519
|
+
if (!memoryEnabled() && memoryModPromise) {
|
|
4520
|
+
await memoryModPromise.then((mod) => mod?.stop?.()).catch(() => {});
|
|
4521
|
+
memoryModPromise = null;
|
|
4522
|
+
}
|
|
4523
|
+
if (session && config.compaction) {
|
|
4524
|
+
session.compaction = {
|
|
4525
|
+
...(session.compaction || {}),
|
|
4526
|
+
...normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() }),
|
|
4527
|
+
};
|
|
4528
|
+
}
|
|
4529
|
+
invalidatePreSessionToolSurface();
|
|
4530
|
+
invalidateContextStatusCache();
|
|
4531
|
+
return this.getMemorySettings();
|
|
4532
|
+
},
|
|
4533
|
+
getChannelSettings(options = {}) {
|
|
4534
|
+
return {
|
|
4535
|
+
enabled: channelsEnabled(),
|
|
4536
|
+
...(options?.includeStatus === false ? {} : { status: channels.status() }),
|
|
4537
|
+
};
|
|
4538
|
+
},
|
|
4539
|
+
async setChannelsEnabled(enabled) {
|
|
4540
|
+
const nextConfig = setModuleEnabledInConfig({ ...config }, 'channels', enabled !== false);
|
|
4541
|
+
saveConfigAndAdopt(nextConfig);
|
|
4542
|
+
if (!channelsEnabled()) {
|
|
4543
|
+
if (channelStartTimer) {
|
|
4544
|
+
clearTimeout(channelStartTimer);
|
|
4545
|
+
channelStartTimer = null;
|
|
4546
|
+
}
|
|
4547
|
+
await channels.stop('settings-disabled', { waitForExit: false }).catch(() => {});
|
|
4548
|
+
} else {
|
|
4549
|
+
scheduleChannelStart(0);
|
|
4550
|
+
}
|
|
4551
|
+
invalidatePreSessionToolSurface();
|
|
4552
|
+
return this.getChannelSettings();
|
|
4553
|
+
},
|
|
2934
4554
|
getSystemShell() {
|
|
2935
4555
|
return normalizeSystemShellConfig(config.shell);
|
|
2936
4556
|
},
|
|
2937
4557
|
setSystemShell(input = {}) {
|
|
2938
4558
|
const command = normalizeSystemShellCommand(typeof input === 'string' ? input : input?.command);
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
...
|
|
2942
|
-
shell: command ? { ...(nextConfig.shell || {}), command } : {},
|
|
4559
|
+
saveConfigAndAdopt({
|
|
4560
|
+
...config,
|
|
4561
|
+
shell: command ? { ...(config.shell || {}), command } : {},
|
|
2943
4562
|
});
|
|
2944
|
-
config = cfgMod.loadConfig();
|
|
2945
4563
|
setConfiguredShell(command);
|
|
2946
4564
|
return normalizeSystemShellConfig(config.shell);
|
|
2947
4565
|
},
|
|
@@ -2960,9 +4578,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2960
4578
|
next.idleMs = idleMs;
|
|
2961
4579
|
if (!hasOwn(input, 'enabled')) next.enabled = true;
|
|
2962
4580
|
}
|
|
2963
|
-
|
|
2964
|
-
cfgMod.saveConfig({ ...nextConfig, autoClear: next });
|
|
2965
|
-
config = cfgMod.loadConfig();
|
|
4581
|
+
saveConfigAndAdopt({ ...config, autoClear: next });
|
|
2966
4582
|
return { ...normalizeAutoClearConfig(config.autoClear), label: formatDurationMs(normalizeAutoClearConfig(config.autoClear).idleMs) };
|
|
2967
4583
|
},
|
|
2968
4584
|
async completeOnboarding(payload = {}) {
|
|
@@ -2970,7 +4586,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2970
4586
|
const workflowInput = payload.workflowRoutes && typeof payload.workflowRoutes === 'object'
|
|
2971
4587
|
? payload.workflowRoutes
|
|
2972
4588
|
: {};
|
|
2973
|
-
const nextConfig =
|
|
4589
|
+
const nextConfig = { ...config };
|
|
2974
4590
|
let presets = Array.isArray(nextConfig.presets) ? nextConfig.presets.slice() : [];
|
|
2975
4591
|
const workflowRoutes = { ...(nextConfig.workflowRoutes || {}) };
|
|
2976
4592
|
|
|
@@ -2989,10 +4605,13 @@ function parsedProviderModelVersion(id) {
|
|
|
2989
4605
|
|
|
2990
4606
|
nextConfig.presets = presets;
|
|
2991
4607
|
nextConfig.workflowRoutes = workflowRoutes;
|
|
4608
|
+
// Maintenance slots store a direct {provider, model} route. Reuse the
|
|
4609
|
+
// onboarding workflow routes directly; only overwrite a slot when its
|
|
4610
|
+
// workflow route was actually provided, otherwise keep the existing slot.
|
|
2992
4611
|
nextConfig.maintenance = {
|
|
2993
4612
|
...(nextConfig.maintenance || {}),
|
|
2994
|
-
|
|
2995
|
-
|
|
4613
|
+
...(workflowRoutes.explorer ? { explore: normalizeWorkflowRoute(workflowRoutes.explorer) } : {}),
|
|
4614
|
+
...(workflowRoutes.memory ? { memory: normalizeWorkflowRoute(workflowRoutes.memory) } : {}),
|
|
2996
4615
|
};
|
|
2997
4616
|
nextConfig.onboarding = {
|
|
2998
4617
|
...(nextConfig.onboarding || {}),
|
|
@@ -3001,8 +4620,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3001
4620
|
completedAt: new Date().toISOString(),
|
|
3002
4621
|
};
|
|
3003
4622
|
|
|
3004
|
-
|
|
3005
|
-
config = cfgMod.loadConfig();
|
|
4623
|
+
saveConfigAndAdopt(nextConfig);
|
|
3006
4624
|
if (defaultRoute) {
|
|
3007
4625
|
route = resolveRoute(config, { provider: defaultRoute.provider, model: defaultRoute.model, effort: defaultRoute.effort });
|
|
3008
4626
|
if (session?.id) mgr.closeSession(session.id, 'cli-onboarding-complete');
|
|
@@ -3085,26 +4703,34 @@ function parsedProviderModelVersion(id) {
|
|
|
3085
4703
|
const result = String(secret || '').trim()
|
|
3086
4704
|
? saveProviderApiKey(cfgMod, providerId, secret)
|
|
3087
4705
|
: await loginOAuthProvider(cfgMod, providerId);
|
|
3088
|
-
|
|
4706
|
+
reloadFullConfig();
|
|
3089
4707
|
invalidateProviderCaches();
|
|
3090
4708
|
warmProviderModelCache();
|
|
3091
4709
|
return result;
|
|
3092
4710
|
},
|
|
3093
4711
|
async loginOAuthProvider(providerId) {
|
|
3094
4712
|
const result = await loginOAuthProvider(cfgMod, providerId);
|
|
3095
|
-
|
|
4713
|
+
reloadFullConfig();
|
|
3096
4714
|
invalidateProviderCaches();
|
|
3097
4715
|
warmProviderModelCache();
|
|
3098
4716
|
return result;
|
|
3099
4717
|
},
|
|
3100
4718
|
async beginOAuthProviderLogin(providerId) {
|
|
3101
4719
|
const result = await beginOAuthProviderLogin(cfgMod, providerId);
|
|
3102
|
-
|
|
4720
|
+
reloadFullConfig();
|
|
3103
4721
|
return {
|
|
3104
4722
|
...result,
|
|
4723
|
+
waitForCallback: result.waitForCallback?.then((completed) => {
|
|
4724
|
+
reloadFullConfig();
|
|
4725
|
+
if (completed) {
|
|
4726
|
+
invalidateProviderCaches();
|
|
4727
|
+
warmProviderModelCache();
|
|
4728
|
+
}
|
|
4729
|
+
return completed;
|
|
4730
|
+
}),
|
|
3105
4731
|
completeCode: async (code) => {
|
|
3106
4732
|
const completed = await result.completeCode(code);
|
|
3107
|
-
|
|
4733
|
+
reloadFullConfig();
|
|
3108
4734
|
invalidateProviderCaches();
|
|
3109
4735
|
warmProviderModelCache();
|
|
3110
4736
|
return completed;
|
|
@@ -3113,42 +4739,97 @@ function parsedProviderModelVersion(id) {
|
|
|
3113
4739
|
},
|
|
3114
4740
|
saveProviderApiKey(providerId, secret) {
|
|
3115
4741
|
const result = saveProviderApiKey(cfgMod, providerId, secret);
|
|
3116
|
-
|
|
4742
|
+
reloadFullConfig();
|
|
3117
4743
|
invalidateProviderCaches();
|
|
3118
4744
|
warmProviderModelCache();
|
|
3119
4745
|
return result;
|
|
3120
4746
|
},
|
|
3121
4747
|
saveOpenAIUsageSessionKey(secret) {
|
|
3122
4748
|
const result = saveOpenAIUsageSessionKey(cfgMod, secret);
|
|
3123
|
-
|
|
4749
|
+
reloadFullConfig();
|
|
3124
4750
|
invalidateProviderCaches();
|
|
3125
4751
|
return result;
|
|
3126
4752
|
},
|
|
3127
4753
|
saveOpenCodeGoUsageAuth(opts) {
|
|
3128
4754
|
const result = saveOpenCodeGoUsageAuth(cfgMod, opts);
|
|
3129
|
-
|
|
4755
|
+
reloadFullConfig();
|
|
3130
4756
|
invalidateProviderCaches();
|
|
3131
4757
|
return result;
|
|
3132
4758
|
},
|
|
3133
4759
|
setLocalProvider(providerId, opts) {
|
|
3134
4760
|
const result = setLocalProvider(cfgMod, providerId, opts);
|
|
3135
|
-
|
|
4761
|
+
reloadFullConfig();
|
|
3136
4762
|
invalidateProviderCaches();
|
|
3137
4763
|
warmProviderModelCache();
|
|
3138
4764
|
return result;
|
|
3139
4765
|
},
|
|
3140
4766
|
forgetProviderAuth(providerId) {
|
|
3141
|
-
const result = forgetProviderAuth(providerId);
|
|
3142
|
-
|
|
4767
|
+
const result = forgetProviderAuth(cfgMod, providerId);
|
|
4768
|
+
reloadFullConfig();
|
|
3143
4769
|
invalidateProviderCaches();
|
|
3144
4770
|
warmProviderModelCache();
|
|
3145
4771
|
return result;
|
|
3146
4772
|
},
|
|
3147
4773
|
listPresets() {
|
|
3148
|
-
return cfgMod.listPresets(
|
|
4774
|
+
return cfgMod.listPresets(displayConfig());
|
|
3149
4775
|
},
|
|
3150
4776
|
async listProviderModels(options = {}) {
|
|
3151
|
-
return await collectProviderModels({
|
|
4777
|
+
return await collectProviderModels({
|
|
4778
|
+
force: options.force === true || options.refresh === true,
|
|
4779
|
+
quick: options.quick === true,
|
|
4780
|
+
});
|
|
4781
|
+
},
|
|
4782
|
+
getSearchRoute() {
|
|
4783
|
+
searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
4784
|
+
return searchRoute;
|
|
4785
|
+
},
|
|
4786
|
+
async listSearchModels(options = {}) {
|
|
4787
|
+
return await collectSearchProviderModels({ force: options.force === true || options.refresh === true });
|
|
4788
|
+
},
|
|
4789
|
+
async setSearchRoute(next) {
|
|
4790
|
+
let selectedRoute = normalizeSearchRouteConfig(next);
|
|
4791
|
+
if (!selectedRoute && next?.model && searchRoute?.provider) {
|
|
4792
|
+
selectedRoute = normalizeSearchRouteConfig({ ...next, provider: searchRoute.provider });
|
|
4793
|
+
}
|
|
4794
|
+
if (!selectedRoute) throw new Error('search route requires provider and model');
|
|
4795
|
+
if (isDefaultSearchRouteConfig(selectedRoute)) {
|
|
4796
|
+
ensureFullConfig();
|
|
4797
|
+
const routeToSave = normalizeSearchRouteConfig({
|
|
4798
|
+
provider: SEARCH_DEFAULT_PROVIDER,
|
|
4799
|
+
model: SEARCH_DEFAULT_MODEL,
|
|
4800
|
+
...(selectedRoute.toolType ? { toolType: selectedRoute.toolType } : {}),
|
|
4801
|
+
});
|
|
4802
|
+
const nextConfig = { ...config };
|
|
4803
|
+
nextConfig.searchRoute = routeToSave;
|
|
4804
|
+
saveConfigAndAdopt(nextConfig);
|
|
4805
|
+
searchRoute = normalizeSearchRouteConfig(config.searchRoute);
|
|
4806
|
+
invalidateProviderCaches();
|
|
4807
|
+
return searchRoute;
|
|
4808
|
+
}
|
|
4809
|
+
if (!isSearchCapableProvider(selectedRoute.provider)) {
|
|
4810
|
+
throw new Error(`provider "${selectedRoute.provider}" does not support Mixdog native search`);
|
|
4811
|
+
}
|
|
4812
|
+
ensureFullConfig();
|
|
4813
|
+
await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
|
|
4814
|
+
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
4815
|
+
if (!searchCapableFor(selectedRoute.provider, modelMeta)) {
|
|
4816
|
+
throw new Error(`model "${selectedRoute.model}" is not marked as web-search capable`);
|
|
4817
|
+
}
|
|
4818
|
+
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
4819
|
+
const effort = coerceEffortFor(selectedRoute.provider, modelMeta, selectedRoute.effort);
|
|
4820
|
+
selectedRoute = {
|
|
4821
|
+
...selectedRoute,
|
|
4822
|
+
...(effort ? { effort } : {}),
|
|
4823
|
+
fast: fastCapable ? selectedRoute.fast === true : false,
|
|
4824
|
+
};
|
|
4825
|
+
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
4826
|
+
const routeToSave = normalizeSearchRouteConfig(selectedRoute);
|
|
4827
|
+
const nextConfig = { ...config };
|
|
4828
|
+
nextConfig.searchRoute = routeToSave;
|
|
4829
|
+
saveConfigAndAdopt(nextConfig);
|
|
4830
|
+
searchRoute = normalizeSearchRouteConfig(config.searchRoute);
|
|
4831
|
+
invalidateProviderCaches();
|
|
4832
|
+
return searchRoute;
|
|
3152
4833
|
},
|
|
3153
4834
|
listAgents() {
|
|
3154
4835
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
@@ -3160,7 +4841,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3160
4841
|
}));
|
|
3161
4842
|
},
|
|
3162
4843
|
listWorkflows() {
|
|
3163
|
-
const currentConfig =
|
|
4844
|
+
const currentConfig = displayConfig();
|
|
3164
4845
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
3165
4846
|
const active = activeWorkflowId(currentConfig);
|
|
3166
4847
|
return listWorkflowPacks(dataDir).map((workflow) => ({
|
|
@@ -3173,16 +4854,13 @@ function parsedProviderModelVersion(id) {
|
|
|
3173
4854
|
}));
|
|
3174
4855
|
},
|
|
3175
4856
|
getOutputStyle() {
|
|
3176
|
-
|
|
3177
|
-
return outputStyleStatus(dataDir);
|
|
4857
|
+
return getOutputStyleStatusCached();
|
|
3178
4858
|
},
|
|
3179
4859
|
listOutputStyles() {
|
|
3180
|
-
|
|
3181
|
-
return outputStyleStatus(dataDir);
|
|
4860
|
+
return getOutputStyleStatusCached();
|
|
3182
4861
|
},
|
|
3183
4862
|
async setOutputStyle(value) {
|
|
3184
|
-
const
|
|
3185
|
-
const before = outputStyleStatus(dataDir);
|
|
4863
|
+
const before = getOutputStyleStatusCached({ fresh: true });
|
|
3186
4864
|
const selected = findOutputStyle(value, before.styles);
|
|
3187
4865
|
if (!selected) {
|
|
3188
4866
|
const names = before.styles.map((style) => style.label || style.id).join(', ') || 'Default';
|
|
@@ -3198,6 +4876,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3198
4876
|
}
|
|
3199
4877
|
return next;
|
|
3200
4878
|
});
|
|
4879
|
+
invalidateOutputStyleStatusCache();
|
|
3201
4880
|
const hasConversation = sessionHasConversationMessages(session);
|
|
3202
4881
|
let appliedToCurrentSession = !hasConversation;
|
|
3203
4882
|
if (session?.id && !hasConversation) {
|
|
@@ -3206,23 +4885,17 @@ function parsedProviderModelVersion(id) {
|
|
|
3206
4885
|
await recreateCurrentSessionIfReady();
|
|
3207
4886
|
}
|
|
3208
4887
|
invalidateContextStatusCache();
|
|
3209
|
-
return { ...
|
|
4888
|
+
return { ...getOutputStyleStatusCached({ fresh: true }), appliedToCurrentSession };
|
|
3210
4889
|
},
|
|
3211
4890
|
async setWorkflow(workflowId) {
|
|
3212
4891
|
const id = normalizeWorkflowId(workflowId, DEFAULT_WORKFLOW_ID);
|
|
3213
4892
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
3214
4893
|
const pack = loadWorkflowPack(dataDir, id);
|
|
3215
4894
|
if (!pack || pack.id !== id) throw new Error(`workflow "${workflowId}" not found`);
|
|
3216
|
-
const nextConfig =
|
|
4895
|
+
const nextConfig = { ...config };
|
|
3217
4896
|
nextConfig.workflow = { ...(nextConfig.workflow || {}), active: id };
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
if (session?.id) {
|
|
3221
|
-
mgr.closeSession(session.id, 'cli-workflow-switch');
|
|
3222
|
-
session = null;
|
|
3223
|
-
}
|
|
3224
|
-
await recreateCurrentSessionIfReady();
|
|
3225
|
-
return { id: pack.id, name: pack.name, description: pack.description, source: pack.source };
|
|
4897
|
+
saveConfigAndAdopt(nextConfig);
|
|
4898
|
+
return workflowSummary(pack);
|
|
3226
4899
|
},
|
|
3227
4900
|
async setAgentRoute(agentId, next) {
|
|
3228
4901
|
const id = normalizeAgentId(agentId);
|
|
@@ -3232,12 +4905,12 @@ function parsedProviderModelVersion(id) {
|
|
|
3232
4905
|
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
3233
4906
|
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
3234
4907
|
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
3235
|
-
|
|
4908
|
+
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
3236
4909
|
|
|
3237
4910
|
const routeToSave = normalizeWorkflowRoute(selectedRoute);
|
|
3238
4911
|
if (!routeToSave) throw new Error('agent route requires provider and model');
|
|
3239
4912
|
const agent = FIXED_AGENT_SLOTS.find((item) => item.id === id);
|
|
3240
|
-
const nextConfig =
|
|
4913
|
+
const nextConfig = { ...config };
|
|
3241
4914
|
nextConfig.agents = {
|
|
3242
4915
|
...(nextConfig.agents || {}),
|
|
3243
4916
|
[id]: routeToSave,
|
|
@@ -3249,24 +4922,48 @@ function parsedProviderModelVersion(id) {
|
|
|
3249
4922
|
[agent.workflowSlot]: routeToSave,
|
|
3250
4923
|
};
|
|
3251
4924
|
nextConfig.presets = upsertWorkflowPreset(nextConfig.presets, agent.workflowSlot, routeToSave);
|
|
4925
|
+
// Maintenance slots store a direct {provider, model} route now, so
|
|
4926
|
+
// mirror the agent route straight in instead of a preset-id string.
|
|
3252
4927
|
nextConfig.maintenance = {
|
|
3253
4928
|
...(nextConfig.maintenance || {}),
|
|
3254
|
-
...(id === 'explore' ? { explore:
|
|
3255
|
-
...(id === 'maintainer' ? { memory:
|
|
4929
|
+
...(id === 'explore' ? { explore: routeToSave } : {}),
|
|
4930
|
+
...(id === 'maintainer' ? { memory: routeToSave } : {}),
|
|
3256
4931
|
};
|
|
3257
4932
|
}
|
|
3258
|
-
|
|
3259
|
-
config = cfgMod.loadConfig();
|
|
4933
|
+
saveConfigAndAdopt(nextConfig);
|
|
3260
4934
|
return routeToSave;
|
|
3261
4935
|
},
|
|
3262
4936
|
async ask(prompt, options = {}) {
|
|
3263
4937
|
activeTurnCount += 1;
|
|
4938
|
+
// Lazy code-graph prewarm: kick off the build ONCE, on the first real
|
|
4939
|
+
// turn, so a likely code lookup hits a warm cache — without paying the
|
|
4940
|
+
// post-first-frame prewarm freeze on idle startup. The schedule is async
|
|
4941
|
+
// + unref'd (worker thread), so it never blocks this turn.
|
|
4942
|
+
if (codeGraphPrewarmLazy && !codeGraphFirstTurnPrewarmDone) {
|
|
4943
|
+
codeGraphFirstTurnPrewarmDone = true;
|
|
4944
|
+
scheduleCodeGraphPrewarm(0, 'first-turn');
|
|
4945
|
+
}
|
|
3264
4946
|
const startedAt = Date.now();
|
|
3265
4947
|
try {
|
|
3266
4948
|
await refreshSessionForCwdIfNeeded('cwd-change');
|
|
3267
4949
|
if (!session?.id) await createCurrentSession('turn');
|
|
3268
4950
|
hooks.emit('turn:start', { sessionId: session.id, prompt, cwd: currentCwd });
|
|
3269
|
-
|
|
4951
|
+
// UserPromptSubmit: bridge to the standard hook bus. A hook FAILURE
|
|
4952
|
+
// must not block the turn, but a genuine blocked===true MUST throw.
|
|
4953
|
+
let promptDispatch = null;
|
|
4954
|
+
try {
|
|
4955
|
+
promptDispatch = await hooks.dispatch('UserPromptSubmit', hookCommonPayload({ session_id: session.id, prompt }));
|
|
4956
|
+
} catch { /* hook failure never blocks the turn */ }
|
|
4957
|
+
if (promptDispatch?.blocked === true) {
|
|
4958
|
+
throw new Error(`prompt blocked by hook: ${promptDispatch.reason || ''}`);
|
|
4959
|
+
}
|
|
4960
|
+
const hookContext = Array.isArray(promptDispatch?.additionalContext)
|
|
4961
|
+
? promptDispatch.additionalContext.join('\n\n')
|
|
4962
|
+
: String(promptDispatch?.additionalContext || '');
|
|
4963
|
+
const turnContext = [options.context || '', hookContext]
|
|
4964
|
+
.map((part) => String(part || '').trim())
|
|
4965
|
+
.filter(Boolean)
|
|
4966
|
+
.join('\n\n');
|
|
3270
4967
|
const result = await mgr.askSession(
|
|
3271
4968
|
session.id,
|
|
3272
4969
|
prompt,
|
|
@@ -3289,16 +4986,25 @@ function parsedProviderModelVersion(id) {
|
|
|
3289
4986
|
{
|
|
3290
4987
|
onTextDelta: options.onTextDelta,
|
|
3291
4988
|
onReasoningDelta: options.onReasoningDelta,
|
|
4989
|
+
onAssistantText: options.onAssistantText,
|
|
3292
4990
|
onUsageDelta: options.onUsageDelta,
|
|
3293
4991
|
onToolResult: options.onToolResult,
|
|
4992
|
+
onToolApproval: options.onToolApproval,
|
|
4993
|
+
onCompactEvent: options.onCompactEvent,
|
|
3294
4994
|
onStageChange: options.onStageChange,
|
|
3295
4995
|
onStreamDelta: options.onStreamDelta,
|
|
3296
4996
|
drainSteering: options.drainSteering,
|
|
3297
4997
|
onSteerMessage: options.onSteerMessage,
|
|
4998
|
+
notifyFn: notifyFnForSession(session.id),
|
|
3298
4999
|
},
|
|
3299
5000
|
);
|
|
3300
5001
|
session = mgr.getSession(session.id) || session;
|
|
3301
5002
|
hooks.emit('turn:end', { sessionId: session.id, elapsedMs: Date.now() - startedAt });
|
|
5003
|
+
// Stop: bridge to the standard hook bus. Best-effort; ignore result,
|
|
5004
|
+
// never throw.
|
|
5005
|
+
try {
|
|
5006
|
+
await hooks.dispatch('Stop', hookCommonPayload({ session_id: session.id }));
|
|
5007
|
+
} catch { /* best-effort: Stop hook must never break the turn */ }
|
|
3302
5008
|
return { result, session };
|
|
3303
5009
|
} catch (error) {
|
|
3304
5010
|
hooks.emit('turn:error', { sessionId: session?.id || null, elapsedMs: Date.now() - startedAt, error: error?.message || String(error) });
|
|
@@ -3307,27 +5013,31 @@ function parsedProviderModelVersion(id) {
|
|
|
3307
5013
|
activeTurnCount = Math.max(0, activeTurnCount - 1);
|
|
3308
5014
|
if (!firstTurnCompleted) {
|
|
3309
5015
|
firstTurnCompleted = true;
|
|
5016
|
+
scheduleProviderWarmup();
|
|
3310
5017
|
scheduleProviderModelWarmup();
|
|
3311
5018
|
}
|
|
3312
5019
|
}
|
|
3313
5020
|
},
|
|
3314
5021
|
async clear(options = {}) {
|
|
3315
5022
|
if (!session?.id) return false;
|
|
3316
|
-
const cleared = await mgr.clearSessionMessages(session.id);
|
|
5023
|
+
const cleared = await mgr.clearSessionMessages(session.id, options);
|
|
3317
5024
|
if (!cleared) return false;
|
|
3318
5025
|
session = typeof cleared === 'object' ? cleared : (mgr.getSession(session.id) || session);
|
|
3319
|
-
if (options.
|
|
3320
|
-
try {
|
|
5026
|
+
if (options.recoverAgent === true) {
|
|
5027
|
+
try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
|
|
3321
5028
|
}
|
|
3322
5029
|
invalidateContextStatusCache();
|
|
3323
5030
|
return true;
|
|
3324
5031
|
},
|
|
3325
5032
|
async compact(options = {}) {
|
|
3326
5033
|
if (!session?.id) return null;
|
|
5034
|
+
if (activeTurnCount > 0) {
|
|
5035
|
+
return { changed: false, reason: 'compact skipped: turn in progress' };
|
|
5036
|
+
}
|
|
3327
5037
|
const result = await mgr.compactSessionMessages(session.id);
|
|
3328
5038
|
session = mgr.getSession(session.id) || session;
|
|
3329
|
-
if (options.
|
|
3330
|
-
try {
|
|
5039
|
+
if (options.recoverAgent === true) {
|
|
5040
|
+
try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
|
|
3331
5041
|
}
|
|
3332
5042
|
invalidateContextStatusCache();
|
|
3333
5043
|
return result;
|
|
@@ -3339,25 +5049,15 @@ function parsedProviderModelVersion(id) {
|
|
|
3339
5049
|
await recreateCurrentSessionIfReady();
|
|
3340
5050
|
return mode;
|
|
3341
5051
|
},
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
try { sharedCfgMod.updateSection('agent', (s) => ({ ...(s || {}), bridgeMode: applied })); } catch {}
|
|
3345
|
-
return applied;
|
|
3346
|
-
},
|
|
3347
|
-
toggleBridgeMode() {
|
|
3348
|
-
const applied = bridge.toggleDefaultMode();
|
|
3349
|
-
try { sharedCfgMod.updateSection('agent', (s) => ({ ...(s || {}), bridgeMode: applied })); } catch {}
|
|
3350
|
-
return applied;
|
|
3351
|
-
},
|
|
3352
|
-
bridgeStatus() {
|
|
3353
|
-
return bridgeStatusState();
|
|
5052
|
+
agentStatus() {
|
|
5053
|
+
return agentStatusState();
|
|
3354
5054
|
},
|
|
3355
5055
|
get clientHostPid() {
|
|
3356
5056
|
return session?.clientHostPid || process.pid;
|
|
3357
5057
|
},
|
|
3358
|
-
|
|
5058
|
+
agentControl(args = {}) {
|
|
3359
5059
|
const callerSessionId = session?.id || null;
|
|
3360
|
-
return
|
|
5060
|
+
return agentTool.execute(args, {
|
|
3361
5061
|
callerCwd: currentCwd,
|
|
3362
5062
|
invocationSource: 'user-command',
|
|
3363
5063
|
callerSessionId,
|
|
@@ -3387,6 +5087,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3387
5087
|
activeCount: rows.filter((row) => row.active).length,
|
|
3388
5088
|
tools,
|
|
3389
5089
|
activeTools: sortedNamesByMeasuredUsage(activeNames),
|
|
5090
|
+
discoveredTools: sortedNamesByMeasuredUsage(surface?.deferredDiscoveredTools || []),
|
|
3390
5091
|
};
|
|
3391
5092
|
},
|
|
3392
5093
|
selectTools(names) {
|
|
@@ -3402,7 +5103,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3402
5103
|
return mcpStatus();
|
|
3403
5104
|
},
|
|
3404
5105
|
async reconnectMcp() {
|
|
3405
|
-
|
|
5106
|
+
reloadFullConfig();
|
|
3406
5107
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3407
5108
|
invalidatePreSessionToolSurface();
|
|
3408
5109
|
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-reconnect');
|
|
@@ -3411,13 +5112,12 @@ function parsedProviderModelVersion(id) {
|
|
|
3411
5112
|
},
|
|
3412
5113
|
async addMcpServer(input = {}) {
|
|
3413
5114
|
const { name, config: serverConfig } = normalizeMcpServerInput(input);
|
|
3414
|
-
const nextConfig =
|
|
5115
|
+
const nextConfig = { ...config };
|
|
3415
5116
|
nextConfig.mcpServers = {
|
|
3416
5117
|
...(nextConfig.mcpServers || {}),
|
|
3417
5118
|
[name]: serverConfig,
|
|
3418
5119
|
};
|
|
3419
|
-
|
|
3420
|
-
config = cfgMod.loadConfig();
|
|
5120
|
+
saveConfigAndAdopt(nextConfig);
|
|
3421
5121
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3422
5122
|
invalidatePreSessionToolSurface();
|
|
3423
5123
|
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-add');
|
|
@@ -3427,7 +5127,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3427
5127
|
async removeMcpServer(name) {
|
|
3428
5128
|
const serverName = clean(name);
|
|
3429
5129
|
if (!serverName) throw new Error('MCP server name is required');
|
|
3430
|
-
const nextConfig =
|
|
5130
|
+
const nextConfig = { ...config };
|
|
3431
5131
|
const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
|
|
3432
5132
|
? { ...nextConfig.mcpServers }
|
|
3433
5133
|
: {};
|
|
@@ -3435,8 +5135,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3435
5135
|
throw new Error(`MCP server not configured: ${serverName}`);
|
|
3436
5136
|
}
|
|
3437
5137
|
delete current[serverName];
|
|
3438
|
-
|
|
3439
|
-
config = cfgMod.loadConfig();
|
|
5138
|
+
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
3440
5139
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3441
5140
|
invalidatePreSessionToolSurface();
|
|
3442
5141
|
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-remove');
|
|
@@ -3446,7 +5145,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3446
5145
|
async setMcpServerEnabled(name, enabled) {
|
|
3447
5146
|
const serverName = clean(name);
|
|
3448
5147
|
if (!serverName) throw new Error('MCP server name is required');
|
|
3449
|
-
const nextConfig =
|
|
5148
|
+
const nextConfig = { ...config };
|
|
3450
5149
|
const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
|
|
3451
5150
|
? { ...nextConfig.mcpServers }
|
|
3452
5151
|
: {};
|
|
@@ -3454,8 +5153,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3454
5153
|
throw new Error(`MCP server not configured: ${serverName}`);
|
|
3455
5154
|
}
|
|
3456
5155
|
current[serverName] = { ...(current[serverName] || {}), enabled: enabled !== false };
|
|
3457
|
-
|
|
3458
|
-
config = cfgMod.loadConfig();
|
|
5156
|
+
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
3459
5157
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3460
5158
|
invalidatePreSessionToolSurface();
|
|
3461
5159
|
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-toggle');
|
|
@@ -3506,13 +5204,12 @@ function parsedProviderModelVersion(id) {
|
|
|
3506
5204
|
const key = clean(plugin.id || plugin.name || plugin);
|
|
3507
5205
|
const dataDir = cfgMod.getPluginData?.();
|
|
3508
5206
|
const removed = registryRemovePlugin(key, { dataDir });
|
|
3509
|
-
const nextConfig =
|
|
5207
|
+
const nextConfig = { ...config };
|
|
3510
5208
|
const serverName = pluginMcpServerName(plugin);
|
|
3511
5209
|
if (nextConfig.mcpServers && Object.prototype.hasOwnProperty.call(nextConfig.mcpServers, serverName)) {
|
|
3512
5210
|
const current = { ...nextConfig.mcpServers };
|
|
3513
5211
|
delete current[serverName];
|
|
3514
|
-
|
|
3515
|
-
config = cfgMod.loadConfig();
|
|
5212
|
+
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
3516
5213
|
await connectConfiguredMcp({ reset: true });
|
|
3517
5214
|
invalidatePreSessionToolSurface();
|
|
3518
5215
|
}
|
|
@@ -3527,7 +5224,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3527
5224
|
const scriptPath = join(root, script);
|
|
3528
5225
|
if (!existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${scriptPath}`);
|
|
3529
5226
|
const serverName = pluginMcpServerName(plugin);
|
|
3530
|
-
const nextConfig =
|
|
5227
|
+
const nextConfig = { ...config };
|
|
3531
5228
|
nextConfig.mcpServers = {
|
|
3532
5229
|
...(nextConfig.mcpServers || {}),
|
|
3533
5230
|
[serverName]: {
|
|
@@ -3540,8 +5237,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3540
5237
|
},
|
|
3541
5238
|
},
|
|
3542
5239
|
};
|
|
3543
|
-
|
|
3544
|
-
config = cfgMod.loadConfig();
|
|
5240
|
+
saveConfigAndAdopt(nextConfig);
|
|
3545
5241
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3546
5242
|
invalidatePreSessionToolSurface();
|
|
3547
5243
|
if (session?.id) mgr.closeSession(session.id, 'cli-plugin-mcp-enable');
|
|
@@ -3609,8 +5305,11 @@ function parsedProviderModelVersion(id) {
|
|
|
3609
5305
|
}
|
|
3610
5306
|
return result;
|
|
3611
5307
|
},
|
|
3612
|
-
async setRoute(next) {
|
|
5308
|
+
async setRoute(next, options = {}) {
|
|
5309
|
+
const applyToCurrentSession = options?.applyToCurrentSession !== false;
|
|
3613
5310
|
const requested = { ...(next || {}) };
|
|
5311
|
+
validateRequestedModelSelector(config, requested);
|
|
5312
|
+
const providerExplicitlyRequested = clean(next?.provider) !== '';
|
|
3614
5313
|
if (requested.effort === undefined && !requested.provider && !requested.model && hasOwn(route, 'effort')) {
|
|
3615
5314
|
requested.effort = route.effort;
|
|
3616
5315
|
}
|
|
@@ -3620,14 +5319,34 @@ function parsedProviderModelVersion(id) {
|
|
|
3620
5319
|
let selectedRoute = resolveRoute(config, requested);
|
|
3621
5320
|
await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
|
|
3622
5321
|
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
5322
|
+
// Guard (A안 / strict reject): a free-text string must never become the
|
|
5323
|
+
// lead model. When the caller did not pin a provider, the route did not
|
|
5324
|
+
// match a preset, and the model is unknown to both the live provider
|
|
5325
|
+
// catalog (modelMeta is still the {id,provider} placeholder) and the
|
|
5326
|
+
// offline catalog, refuse BEFORE any config write so a partial save
|
|
5327
|
+
// (saveModelSettings/adoptConfig/persistLeadRoute) can never land.
|
|
5328
|
+
if (!providerExplicitlyRequested
|
|
5329
|
+
&& !selectedRoute.preset
|
|
5330
|
+
&& !modelMetaLooksResolved(modelMeta)
|
|
5331
|
+
&& !getModelMetadataSync(selectedRoute.model, selectedRoute.provider)) {
|
|
5332
|
+
throw new Error(`unknown model: ${selectedRoute.provider}/${selectedRoute.model}`);
|
|
5333
|
+
}
|
|
3623
5334
|
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
3624
5335
|
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
3625
|
-
|
|
5336
|
+
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
5337
|
+
if (!applyToCurrentSession) {
|
|
5338
|
+
persistLeadRoute(selectedRoute);
|
|
5339
|
+
refreshStatuslineUsageSnapshot(route);
|
|
5340
|
+
scheduleStatuslineUsageRefresh();
|
|
5341
|
+
return selectedRoute;
|
|
5342
|
+
}
|
|
3626
5343
|
const leadRoute = persistLeadRoute(selectedRoute);
|
|
3627
5344
|
route = resolveRoute(config, leadRoute
|
|
3628
5345
|
? { model: workflowPresetId('lead') }
|
|
3629
5346
|
: selectedRoute);
|
|
3630
5347
|
await refreshRouteEffort(modelMeta);
|
|
5348
|
+
refreshStatuslineUsageSnapshot(route);
|
|
5349
|
+
scheduleStatuslineUsageRefresh();
|
|
3631
5350
|
if (session) {
|
|
3632
5351
|
const updated = mgr.updateSessionRoute?.(session.id, {
|
|
3633
5352
|
provider: route.provider,
|
|
@@ -3642,7 +5361,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3642
5361
|
session.fast = route.fast === true;
|
|
3643
5362
|
session.effort = route.effectiveEffort || null;
|
|
3644
5363
|
}
|
|
3645
|
-
statusRoutes
|
|
5364
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
3646
5365
|
invalidateContextStatusCache();
|
|
3647
5366
|
}
|
|
3648
5367
|
return route;
|
|
@@ -3656,14 +5375,14 @@ function parsedProviderModelVersion(id) {
|
|
|
3656
5375
|
throw new Error(`fast mode is not available for ${route.provider}/${route.model}`);
|
|
3657
5376
|
}
|
|
3658
5377
|
route = resolveRoute(config, { provider: route.provider, model: route.model, effort: route.effort, fast: fastCapable ? enabled : false });
|
|
3659
|
-
|
|
5378
|
+
adoptConfig(saveModelSettings(cfgMod, route, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
3660
5379
|
const leadRoute = persistLeadRoute(route);
|
|
3661
5380
|
if (leadRoute) route = resolveRoute(config, { model: workflowPresetId('lead') });
|
|
3662
5381
|
await refreshRouteEffort(modelMeta);
|
|
3663
5382
|
if (session) {
|
|
3664
5383
|
session.fast = route.fast === true;
|
|
3665
5384
|
session.effort = route.effectiveEffort || null;
|
|
3666
|
-
statusRoutes
|
|
5385
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
3667
5386
|
invalidateContextStatusCache();
|
|
3668
5387
|
}
|
|
3669
5388
|
return route.fast === true;
|
|
@@ -3676,15 +5395,17 @@ function parsedProviderModelVersion(id) {
|
|
|
3676
5395
|
async setEffort(value) {
|
|
3677
5396
|
const normalized = normalizeEffortInput(value);
|
|
3678
5397
|
route = { ...route, effort: normalized };
|
|
3679
|
-
|
|
5398
|
+
const modelMeta = await lookupModelMeta(route.provider, route.model);
|
|
5399
|
+
const fastCapable = fastCapableFor(route.provider, modelMeta);
|
|
5400
|
+
adoptConfig(saveModelSettings(cfgMod, route, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
3680
5401
|
const leadRoute = persistLeadRoute(route);
|
|
3681
5402
|
if (leadRoute) {
|
|
3682
5403
|
route = resolveRoute(config, { model: workflowPresetId('lead') });
|
|
3683
5404
|
}
|
|
3684
|
-
await refreshRouteEffort();
|
|
5405
|
+
await refreshRouteEffort(modelMeta);
|
|
3685
5406
|
if (session) {
|
|
3686
5407
|
session.effort = route.effectiveEffort || null;
|
|
3687
|
-
statusRoutes
|
|
5408
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
3688
5409
|
invalidateContextStatusCache();
|
|
3689
5410
|
}
|
|
3690
5411
|
return route;
|
|
@@ -3692,10 +5413,18 @@ function parsedProviderModelVersion(id) {
|
|
|
3692
5413
|
async close(reason = 'cli-exit', options = {}) {
|
|
3693
5414
|
const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
|
|
3694
5415
|
closeRequested = true;
|
|
5416
|
+
// Persist any change that is still sitting in the debounce window so a
|
|
5417
|
+
// toggle made right before exit is not lost. Synchronous + best-effort:
|
|
5418
|
+
// teardown must continue even if the final write fails.
|
|
5419
|
+
try { flushConfigSave(); } catch {}
|
|
3695
5420
|
if (channelStartTimer) {
|
|
3696
5421
|
clearTimeout(channelStartTimer);
|
|
3697
5422
|
channelStartTimer = null;
|
|
3698
5423
|
}
|
|
5424
|
+
if (providerSetupWarmupTimer) {
|
|
5425
|
+
clearTimeout(providerSetupWarmupTimer);
|
|
5426
|
+
providerSetupWarmupTimer = null;
|
|
5427
|
+
}
|
|
3699
5428
|
if (providerWarmupTimer) {
|
|
3700
5429
|
clearTimeout(providerWarmupTimer);
|
|
3701
5430
|
providerWarmupTimer = null;
|
|
@@ -3704,9 +5433,21 @@ function parsedProviderModelVersion(id) {
|
|
|
3704
5433
|
clearTimeout(providerModelWarmupTimer);
|
|
3705
5434
|
providerModelWarmupTimer = null;
|
|
3706
5435
|
}
|
|
5436
|
+
if (codeGraphPrewarmTimer) {
|
|
5437
|
+
clearTimeout(codeGraphPrewarmTimer);
|
|
5438
|
+
codeGraphPrewarmTimer = null;
|
|
5439
|
+
}
|
|
5440
|
+
if (statuslineUsageWarmupTimer) {
|
|
5441
|
+
clearTimeout(statuslineUsageWarmupTimer);
|
|
5442
|
+
statuslineUsageWarmupTimer = null;
|
|
5443
|
+
}
|
|
5444
|
+
if (statuslineUsageRefreshTimer) {
|
|
5445
|
+
clearTimeout(statuslineUsageRefreshTimer);
|
|
5446
|
+
statuslineUsageRefreshTimer = null;
|
|
5447
|
+
}
|
|
3707
5448
|
try { cancelBackgroundTasks({ reason, notify: false }); } catch {}
|
|
3708
5449
|
const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
|
|
3709
|
-
try {
|
|
5450
|
+
try { agentTool.closeAll(reason); } catch {}
|
|
3710
5451
|
let mcpStop = null;
|
|
3711
5452
|
try { mcpStop = mcpClient.disconnectAll?.(); } catch {}
|
|
3712
5453
|
const openaiWsStop = globalThis.__mixdogOpenaiWsRuntimeLoaded === true
|
|
@@ -3715,14 +5456,35 @@ function parsedProviderModelVersion(id) {
|
|
|
3715
5456
|
.catch(() => {})
|
|
3716
5457
|
: null;
|
|
3717
5458
|
const patchStop = closePatchRuntimeIfLoaded(detach ? { waitForExit: false } : undefined);
|
|
5459
|
+
const memoryStop = memoryModPromise
|
|
5460
|
+
? memoryModPromise
|
|
5461
|
+
.then((mod) => (typeof mod?.stop === 'function' ? mod.stop() : null))
|
|
5462
|
+
.catch(() => {})
|
|
5463
|
+
.finally(() => {
|
|
5464
|
+
memoryModPromise = null;
|
|
5465
|
+
})
|
|
5466
|
+
: null;
|
|
3718
5467
|
let ok = false;
|
|
3719
5468
|
if (session?.id) {
|
|
3720
5469
|
statusRoutes?.clearGatewaySessionRoute?.(session.id);
|
|
3721
5470
|
ok = mgr.closeSession(session.id, reason);
|
|
3722
5471
|
session = null;
|
|
3723
5472
|
}
|
|
5473
|
+
const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
|
|
5474
|
+
? import('./runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs')
|
|
5475
|
+
.then((mod) => mod?.shutdownShellJobs?.(reason, { sync: !detach }))
|
|
5476
|
+
.catch(() => {})
|
|
5477
|
+
: null;
|
|
5478
|
+
const bashSessionsStop = globalThis.__mixdogBashSessionRuntimeLoaded === true
|
|
5479
|
+
? import('./runtime/agent/orchestrator/tools/bash-session.mjs')
|
|
5480
|
+
.then((mod) => mod?.shutdownBashSessions?.(reason))
|
|
5481
|
+
.catch(() => {})
|
|
5482
|
+
: null;
|
|
3724
5483
|
if (detach) {
|
|
3725
5484
|
try { await withTeardownDeadline(channelStop, 300, false); } catch {}
|
|
5485
|
+
try { await withTeardownDeadline(shellJobsStop, 300, false); } catch {}
|
|
5486
|
+
try { await withTeardownDeadline(bashSessionsStop, 300, false); } catch {}
|
|
5487
|
+
try { await withTeardownDeadline(memoryStop, 1500, false); } catch {}
|
|
3726
5488
|
for (const stop of [mcpStop, openaiWsStop, patchStop]) {
|
|
3727
5489
|
Promise.resolve(stop).catch(() => {});
|
|
3728
5490
|
}
|
|
@@ -3733,6 +5495,9 @@ function parsedProviderModelVersion(id) {
|
|
|
3733
5495
|
withTeardownDeadline(mcpStop, 1500, false),
|
|
3734
5496
|
withTeardownDeadline(openaiWsStop, 1500, false),
|
|
3735
5497
|
withTeardownDeadline(patchStop, 1500, false),
|
|
5498
|
+
withTeardownDeadline(memoryStop, 5500, false),
|
|
5499
|
+
withTeardownDeadline(shellJobsStop, 1500, false),
|
|
5500
|
+
withTeardownDeadline(bashSessionsStop, 1500, false),
|
|
3736
5501
|
]);
|
|
3737
5502
|
return ok;
|
|
3738
5503
|
},
|
|
@@ -3750,7 +5515,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3750
5515
|
const leadish = role === 'lead'
|
|
3751
5516
|
|| sourceType === 'lead'
|
|
3752
5517
|
|| (sourceType === 'cli' && (!sourceName || sourceName === 'main'))
|
|
3753
|
-
|| (!sourceType && !sourceName && owner
|
|
5518
|
+
|| (!sourceType && !sourceName && !isAgentOwner(owner));
|
|
3754
5519
|
if (!leadish) return null;
|
|
3755
5520
|
let preview = cleanSessionPreview(s.preview || '');
|
|
3756
5521
|
let messageCount = Math.max(0, Number(s.messageCount) || 0);
|
|
@@ -3796,11 +5561,11 @@ function parsedProviderModelVersion(id) {
|
|
|
3796
5561
|
await refreshRouteEffort();
|
|
3797
5562
|
session.effort = route.effectiveEffort || null;
|
|
3798
5563
|
session.cwd = currentCwd;
|
|
3799
|
-
applyDeferredToolSurface(session, mode, standaloneTools);
|
|
5564
|
+
applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
3800
5565
|
invalidatePreSessionToolSurface();
|
|
3801
5566
|
invalidateContextStatusCache();
|
|
3802
5567
|
sessionNeedsCwdRefresh = false;
|
|
3803
|
-
statusRoutes
|
|
5568
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
3804
5569
|
return {
|
|
3805
5570
|
id: resumed.id,
|
|
3806
5571
|
messages: resumed.messages || [],
|