mixdog 0.8.1 → 0.9.0
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 +33 -27
- 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 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2212 -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 +110 -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 +76 -28
- 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 +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -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 +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- 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 +31 -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 +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- 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 +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +520 -98
- 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/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 -1
- 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 +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -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 +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -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 +145 -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 +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4788 -2151
- 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 +367 -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 +592 -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 +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8826 -2390
- 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 +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -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 +746 -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 +8 -4
- 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/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/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/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,93 @@ 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 changed;
|
|
2860
|
+
void markRefresh;
|
|
2861
|
+
// Lazy mode: before the first turn (e.g. the initial project-selection
|
|
2862
|
+
// cwd set), do NOT prewarm — that is exactly the post-first-frame freeze
|
|
2863
|
+
// we are avoiding. Once a turn has run, an in-session cwd switch DOES
|
|
2864
|
+
// prewarm the new dir, since a lookup there is now likely.
|
|
2865
|
+
if (codeGraphPrewarmLazy && !codeGraphFirstTurnPrewarmDone) {
|
|
2866
|
+
bootProfile('code-graph:prewarm-lazy', { reason: 'cwd-deferred-to-first-turn' });
|
|
2867
|
+
} else {
|
|
2868
|
+
scheduleCodeGraphPrewarm(changed ? 0 : codeGraphPrewarmDelayMs, changed ? 'cwd-change' : 'cwd');
|
|
2869
|
+
}
|
|
1854
2870
|
return currentCwd;
|
|
1855
2871
|
}
|
|
1856
2872
|
|
|
1857
2873
|
async function refreshSessionForCwdIfNeeded(reason = 'cwd-change') {
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
session = null;
|
|
2874
|
+
// No-op: cwd changes are applied in place by applyResolvedCwd and never
|
|
2875
|
+
// tear down the session. Retained as a stable hook for ask()'s pre-turn
|
|
2876
|
+
// call so the surrounding turn flow is unchanged.
|
|
2877
|
+
void reason;
|
|
1863
2878
|
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
|
-
}
|
|
2879
|
+
return session;
|
|
1877
2880
|
}
|
|
1878
2881
|
|
|
1879
2882
|
function skillContent(name) {
|
|
@@ -1884,6 +2887,12 @@ export async function createMixdogSessionRuntime({
|
|
|
1884
2887
|
return { name, content };
|
|
1885
2888
|
}
|
|
1886
2889
|
|
|
2890
|
+
function skillToolContent(name) {
|
|
2891
|
+
const skill = skillContent(name);
|
|
2892
|
+
const escapedName = String(skill.name || '').replace(/[<>&]/g, (ch) => ({ '<': '<', '>': '>', '&': '&' }[ch]));
|
|
2893
|
+
return `<skill>\n<name>${escapedName}</name>\n${skill.content}\n</skill>`;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
1887
2896
|
function addProjectSkill(input = {}) {
|
|
1888
2897
|
const name = clean(input.name).replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1889
2898
|
if (!name) throw new Error('skill name is required');
|
|
@@ -1996,30 +3005,25 @@ export async function createMixdogSessionRuntime({
|
|
|
1996
3005
|
return { name, config: { command, args, cwd: resolvedCwd } };
|
|
1997
3006
|
}
|
|
1998
3007
|
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2001
|
-
})();
|
|
2002
|
-
const bridgeStartedAt = performance.now();
|
|
2003
|
-
const bridge = createStandaloneBridge({
|
|
3008
|
+
const agentToolStartedAt = performance.now();
|
|
3009
|
+
const agentTool = createStandaloneAgent({
|
|
2004
3010
|
cfgMod,
|
|
2005
3011
|
reg,
|
|
2006
3012
|
mgr,
|
|
2007
3013
|
dataDir: cfgMod.getPluginData(),
|
|
2008
3014
|
cwd,
|
|
2009
|
-
defaultMode: persistedBridgeMode ?? 'async',
|
|
2010
3015
|
});
|
|
2011
|
-
bootProfile('
|
|
2012
|
-
const
|
|
3016
|
+
bootProfile('agent:ready', { ms: (performance.now() - agentToolStartedAt).toFixed(1) });
|
|
3017
|
+
const agentStatusState = () => {
|
|
2013
3018
|
try {
|
|
2014
|
-
const status =
|
|
3019
|
+
const status = agentTool.getStatus?.({ clientHostPid: session?.clientHostPid || process.pid }) || {};
|
|
2015
3020
|
return {
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
bridgeScope: status.scope || null,
|
|
3021
|
+
agentWorkers: Array.isArray(status.workers) ? status.workers : [],
|
|
3022
|
+
agentJobs: Array.isArray(status.jobs) ? status.jobs : [],
|
|
3023
|
+
agentScope: status.scope || null,
|
|
2020
3024
|
};
|
|
2021
3025
|
} catch {
|
|
2022
|
-
return {
|
|
3026
|
+
return { agentWorkers: [], agentJobs: [], agentScope: null };
|
|
2023
3027
|
}
|
|
2024
3028
|
};
|
|
2025
3029
|
const channelsStartedAt = performance.now();
|
|
@@ -2028,18 +3032,28 @@ export async function createMixdogSessionRuntime({
|
|
|
2028
3032
|
rootDir: STANDALONE_ROOT,
|
|
2029
3033
|
dataDir: cfgMod.getPluginData(),
|
|
2030
3034
|
cwd,
|
|
3035
|
+
onNotify: (msg) => {
|
|
3036
|
+
if (msg?.method !== 'notifications/claude/channel') return;
|
|
3037
|
+
const params = msg?.params && typeof msg.params === 'object' ? msg.params : {};
|
|
3038
|
+
const meta = params.meta && typeof params.meta === 'object' ? params.meta : {};
|
|
3039
|
+
if (meta.silent_to_agent === true || meta.silent_to_agent === 'true') return;
|
|
3040
|
+
const instruction = typeof meta.instruction === 'string' ? meta.instruction.trim() : '';
|
|
3041
|
+
const content = instruction || String(params.content || '').trim();
|
|
3042
|
+
emitRuntimeNotification(content, meta);
|
|
3043
|
+
},
|
|
2031
3044
|
});
|
|
2032
3045
|
bootProfile('channels:worker-ready', { ms: (performance.now() - channelsStartedAt).toFixed(1) });
|
|
2033
3046
|
const toolsStartedAt = performance.now();
|
|
2034
3047
|
const standaloneTools = [
|
|
2035
3048
|
TOOL_SEARCH_TOOL,
|
|
3049
|
+
SKILL_TOOL,
|
|
2036
3050
|
CWD_TOOL,
|
|
2037
3051
|
EXPLORE_TOOL,
|
|
2038
3052
|
...(searchToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'search' || tool?.name === 'web_fetch'),
|
|
2039
3053
|
...(memoryToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'recall' || tool?.name === 'memory'),
|
|
2040
3054
|
...(channelToolDefs?.TOOL_DEFS || []).filter((tool) => channels.isChannelTool(tool?.name)),
|
|
2041
3055
|
...(codeGraphToolDefs?.CODE_GRAPH_TOOL_DEFS || []).filter((tool) => tool?.name === 'code_graph'),
|
|
2042
|
-
...
|
|
3056
|
+
...agentTool.tools,
|
|
2043
3057
|
PROVIDER_STATUS_TOOL,
|
|
2044
3058
|
CHANNEL_STATUS_TOOL,
|
|
2045
3059
|
].map(applyStandaloneToolDefaults);
|
|
@@ -2060,7 +3074,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2060
3074
|
: [];
|
|
2061
3075
|
const tools = filterDisallowedTools(previewTools, LEAD_DISALLOWED_TOOLS);
|
|
2062
3076
|
const surface = { tools: Array.isArray(tools) ? tools.slice() : [] };
|
|
2063
|
-
applyDeferredToolSurface(surface, mode, standaloneTools);
|
|
3077
|
+
applyDeferredToolSurface(surface, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
2064
3078
|
return surface;
|
|
2065
3079
|
}
|
|
2066
3080
|
|
|
@@ -2075,7 +3089,11 @@ export async function createMixdogSessionRuntime({
|
|
|
2075
3089
|
const selected = Array.isArray(preSessionToolSurface.deferredSelectedTools)
|
|
2076
3090
|
? preSessionToolSurface.deferredSelectedTools
|
|
2077
3091
|
: [];
|
|
2078
|
-
|
|
3092
|
+
const discovered = Array.isArray(preSessionToolSurface.deferredDiscoveredTools)
|
|
3093
|
+
? preSessionToolSurface.deferredDiscoveredTools
|
|
3094
|
+
: [];
|
|
3095
|
+
const replay = [...new Set([...selected, ...discovered])];
|
|
3096
|
+
if (replay.length) selectDeferredTools(session, replay, deferredSurfaceModeForLead(mode));
|
|
2079
3097
|
}
|
|
2080
3098
|
internalTools.setInternalToolsProvider({
|
|
2081
3099
|
tools: standaloneTools,
|
|
@@ -2091,35 +3109,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2091
3109
|
routingSessionId: callerSessionId,
|
|
2092
3110
|
clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
|
|
2093
3111
|
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
|
-
}
|
|
3112
|
+
nativeSearch: name === 'search'
|
|
3113
|
+
? async (searchArgs) => runNativeWebSearch(searchArgs, { signal: callerCtx?.signal || session?.controller?.signal })
|
|
2123
3114
|
: undefined,
|
|
2124
3115
|
});
|
|
2125
3116
|
}
|
|
@@ -2133,7 +3124,9 @@ export async function createMixdogSessionRuntime({
|
|
|
2133
3124
|
if (!codeGraphMod?.executeCodeGraphTool) throw new Error('code_graph runtime is not available');
|
|
2134
3125
|
return await codeGraphMod.executeCodeGraphTool(name, args || {}, args?.cwd || callerCwd);
|
|
2135
3126
|
}
|
|
2136
|
-
if (name === 'tool_search')
|
|
3127
|
+
if (name === 'tool_search') {
|
|
3128
|
+
return renderToolSearch(args, activeToolSurface(), mode);
|
|
3129
|
+
}
|
|
2137
3130
|
if (name === 'explore') {
|
|
2138
3131
|
const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
|
|
2139
3132
|
return await runExplore(args || {}, {
|
|
@@ -2153,9 +3146,12 @@ export async function createMixdogSessionRuntime({
|
|
|
2153
3146
|
}
|
|
2154
3147
|
return JSON.stringify({ cwd: currentCwd, sessionId: session?.id || null }, null, 2);
|
|
2155
3148
|
}
|
|
2156
|
-
if (name === '
|
|
3149
|
+
if (name === 'Skill') {
|
|
3150
|
+
return skillToolContent(args?.name);
|
|
3151
|
+
}
|
|
3152
|
+
if (name === 'agent') {
|
|
2157
3153
|
const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
|
|
2158
|
-
return await
|
|
3154
|
+
return await agentTool.execute(args, {
|
|
2159
3155
|
callerCwd,
|
|
2160
3156
|
invocationSource: 'model-tool',
|
|
2161
3157
|
callerSessionId,
|
|
@@ -2164,9 +3160,12 @@ export async function createMixdogSessionRuntime({
|
|
|
2164
3160
|
notifyFn: notifyFnForSession(callerSessionId),
|
|
2165
3161
|
});
|
|
2166
3162
|
}
|
|
2167
|
-
if (name === 'provider_status') return renderProviderStatus(
|
|
3163
|
+
if (name === 'provider_status') return renderProviderStatus(displayConfig());
|
|
2168
3164
|
if (name === 'channel_status') return renderChannelStatus();
|
|
2169
|
-
if (channels.isChannelTool(name))
|
|
3165
|
+
if (channels.isChannelTool(name)) {
|
|
3166
|
+
if (!channelsEnabled()) throw new Error('channels are disabled in settings');
|
|
3167
|
+
return await channels.execute(name, args || {});
|
|
3168
|
+
}
|
|
2170
3169
|
throw new Error(`unknown standalone internal tool: ${name}`);
|
|
2171
3170
|
},
|
|
2172
3171
|
});
|
|
@@ -2185,16 +3184,81 @@ export async function createMixdogSessionRuntime({
|
|
|
2185
3184
|
function invalidateProviderCaches() {
|
|
2186
3185
|
providerModelsCache = { models: null, at: 0 };
|
|
2187
3186
|
providerModelsPromise = null;
|
|
3187
|
+
searchProviderModelsCache = { models: null, at: 0 };
|
|
3188
|
+
searchProviderModelsPromise = null;
|
|
3189
|
+
usageDashboardCache = { dashboard: null, at: 0 };
|
|
3190
|
+
usageDashboardPromise = null;
|
|
2188
3191
|
providerSetupCache = { setup: null, at: 0 };
|
|
3192
|
+
providerSetupQuickCache = { setup: null, at: 0 };
|
|
2189
3193
|
providerSetupPromise = null;
|
|
2190
3194
|
providerInitPromise = null;
|
|
2191
3195
|
modelMetaByRoute.clear();
|
|
2192
3196
|
}
|
|
2193
3197
|
|
|
3198
|
+
function adoptConfig(nextConfig, { hasSecrets = configHasSecrets } = {}) {
|
|
3199
|
+
config = nextConfig;
|
|
3200
|
+
configHasSecrets = hasSecrets === true;
|
|
3201
|
+
setConfiguredShell(normalizeSystemShellConfig(config.shell).command);
|
|
3202
|
+
searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
3203
|
+
return config;
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
// Persisting mixdog-config.json is heavy: it takes a cross-process file
|
|
3207
|
+
// lock, does an atomic temp+rename, and on win32 shells out to icacls
|
|
3208
|
+
// (an external process, multiple times per write) to enforce an owner-only
|
|
3209
|
+
// ACL. Doing that synchronously on EVERY option toggle froze the TUI main
|
|
3210
|
+
// thread, so adjusting a picker felt laggy and unresponsive. We split the
|
|
3211
|
+
// two concerns: adopt the new config in-memory IMMEDIATELY (so getProfile/
|
|
3212
|
+
// getAutoClear/etc. and the UI read fresh values on the same tick), and
|
|
3213
|
+
// DEBOUNCE the actual disk write so a burst of toggles collapses into a
|
|
3214
|
+
// single persist after input settles. The security model is unchanged —
|
|
3215
|
+
// cfgMod.saveConfig still strips apiKey (keychain-only) and still applies
|
|
3216
|
+
// the owner-only ACL; we only changed WHEN it runs, never HOW.
|
|
3217
|
+
let pendingConfigToSave = null;
|
|
3218
|
+
let configSaveTimer = null;
|
|
3219
|
+
const CONFIG_SAVE_DEBOUNCE_MS = 150;
|
|
3220
|
+
function flushConfigSave() {
|
|
3221
|
+
if (configSaveTimer) {
|
|
3222
|
+
clearTimeout(configSaveTimer);
|
|
3223
|
+
configSaveTimer = null;
|
|
3224
|
+
}
|
|
3225
|
+
if (pendingConfigToSave === null) return;
|
|
3226
|
+
const snapshot = pendingConfigToSave;
|
|
3227
|
+
pendingConfigToSave = null;
|
|
3228
|
+
try {
|
|
3229
|
+
cfgMod.saveConfig(snapshot);
|
|
3230
|
+
} catch (err) {
|
|
3231
|
+
process.stderr.write(`[config] debounced saveConfig failed: ${err?.message || err}\n`);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
function saveConfigAndAdopt(nextConfig, { hasSecrets = configHasSecrets } = {}) {
|
|
3235
|
+
// In-memory adopt is synchronous and first so callers that read back the
|
|
3236
|
+
// value immediately (e.g. setProfile -> getProfile) see the new state.
|
|
3237
|
+
const adopted = adoptConfig(nextConfig, { hasSecrets });
|
|
3238
|
+
// Persist the adopted object; coalesce rapid successive changes into one
|
|
3239
|
+
// disk write after CONFIG_SAVE_DEBOUNCE_MS of quiet.
|
|
3240
|
+
pendingConfigToSave = config;
|
|
3241
|
+
if (configSaveTimer) clearTimeout(configSaveTimer);
|
|
3242
|
+
configSaveTimer = setTimeout(flushConfigSave, CONFIG_SAVE_DEBOUNCE_MS);
|
|
3243
|
+
configSaveTimer.unref?.();
|
|
3244
|
+
return adopted;
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
function reloadFullConfig() {
|
|
3248
|
+
// A pending debounced write holds the only copy of the latest change.
|
|
3249
|
+
// Flush it before re-reading from disk so loadConfig() observes (and the
|
|
3250
|
+
// subsequent adopt preserves) that change instead of reverting to a stale
|
|
3251
|
+
// on-disk snapshot.
|
|
3252
|
+
flushConfigSave();
|
|
3253
|
+
return adoptConfig(cfgMod.loadConfig(), { hasSecrets: true });
|
|
3254
|
+
}
|
|
3255
|
+
|
|
2194
3256
|
function ensureFullConfig() {
|
|
2195
3257
|
if (configHasSecrets) return config;
|
|
2196
|
-
|
|
2197
|
-
|
|
3258
|
+
return reloadFullConfig();
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
function displayConfig() {
|
|
2198
3262
|
return config;
|
|
2199
3263
|
}
|
|
2200
3264
|
|
|
@@ -2207,6 +3271,19 @@ export async function createMixdogSessionRuntime({
|
|
|
2207
3271
|
return ensureFullConfig();
|
|
2208
3272
|
}
|
|
2209
3273
|
|
|
3274
|
+
function refreshStatuslineUsageSnapshot(routeLike = {}) {
|
|
3275
|
+
const providerId = clean(routeLike.provider);
|
|
3276
|
+
const modelId = clean(routeLike.model);
|
|
3277
|
+
if (!providerId || !providerId.includes('oauth')) return;
|
|
3278
|
+
const providerObj = reg.getProvider(providerId);
|
|
3279
|
+
if (!providerObj) return;
|
|
3280
|
+
void fetchOAuthUsageSnapshot({ provider: providerId, model: modelId }, providerObj, (message) => {
|
|
3281
|
+
if (process.env.MIXDOG_STATUSLINE_TRACE) {
|
|
3282
|
+
try { process.stderr.write(`[statusline] ${message}\n`); } catch {}
|
|
3283
|
+
}
|
|
3284
|
+
}).catch(() => {});
|
|
3285
|
+
}
|
|
3286
|
+
|
|
2210
3287
|
async function ensureProvidersReady(providerConfig = config.providers || {}) {
|
|
2211
3288
|
if (providerInitPromise) return await providerInitPromise;
|
|
2212
3289
|
providerInitPromise = reg.initProviders(providerConfig)
|
|
@@ -2216,13 +3293,23 @@ export async function createMixdogSessionRuntime({
|
|
|
2216
3293
|
return await providerInitPromise;
|
|
2217
3294
|
}
|
|
2218
3295
|
|
|
2219
|
-
async function cachedProviderSetup({ force = false } = {}) {
|
|
2220
|
-
|
|
2221
|
-
if (!force && providerSetupCache.setup && now - providerSetupCache.at < PROVIDER_SETUP_CACHE_TTL_MS) {
|
|
3296
|
+
async function cachedProviderSetup({ force = false, quick = false } = {}) {
|
|
3297
|
+
if (!force && providerSetupCache.setup) {
|
|
2222
3298
|
return providerSetupCache.setup;
|
|
2223
3299
|
}
|
|
2224
|
-
if (
|
|
2225
|
-
|
|
3300
|
+
if (quick) {
|
|
3301
|
+
if (!force && providerSetupQuickCache.setup) {
|
|
3302
|
+
return providerSetupQuickCache.setup;
|
|
3303
|
+
}
|
|
3304
|
+
const setup = await providerSetup(displayConfig(), { detectLocal: false, checkSecrets: false });
|
|
3305
|
+
providerSetupQuickCache = { setup, at: Date.now() };
|
|
3306
|
+
if (!providerSetupPromise && !providerSetupWarmupTimer && !closeRequested) {
|
|
3307
|
+
scheduleProviderSetupWarmup(0);
|
|
3308
|
+
}
|
|
3309
|
+
return setup;
|
|
3310
|
+
}
|
|
3311
|
+
if (providerSetupPromise) return await providerSetupPromise;
|
|
3312
|
+
providerSetupPromise = providerSetup(displayConfig(), { detectLocal: true })
|
|
2226
3313
|
.then((setup) => {
|
|
2227
3314
|
providerSetupCache = { setup, at: Date.now() };
|
|
2228
3315
|
return setup;
|
|
@@ -2370,9 +3457,10 @@ function parsedProviderModelVersion(id) {
|
|
|
2370
3457
|
description: m.description || '',
|
|
2371
3458
|
supportsVision: m.supportsVision === true,
|
|
2372
3459
|
supportsFunctionCalling: m.supportsFunctionCalling === true,
|
|
3460
|
+
supportsWebSearch: searchCapableFor(name, m),
|
|
2373
3461
|
supportsPromptCaching: m.supportsPromptCaching === true,
|
|
2374
3462
|
supportsReasoning: m.supportsReasoning === true,
|
|
2375
|
-
reasoningLevels: Array.isArray(m.reasoningLevels) ? m.reasoningLevels :
|
|
3463
|
+
reasoningLevels: Array.isArray(m.reasoningLevels) ? m.reasoningLevels : undefined,
|
|
2376
3464
|
reasoningOptions: Array.isArray(m.reasoningOptions) ? m.reasoningOptions : [],
|
|
2377
3465
|
reasoningContentField: m.reasoningContentField || null,
|
|
2378
3466
|
mode: m.mode || null,
|
|
@@ -2394,40 +3482,265 @@ function parsedProviderModelVersion(id) {
|
|
|
2394
3482
|
return sortProviderModels((rows || []).map(hydrateProviderModelRow));
|
|
2395
3483
|
}
|
|
2396
3484
|
|
|
2397
|
-
|
|
3485
|
+
function quickProviderModelRows() {
|
|
3486
|
+
const pickerConfig = displayConfig();
|
|
3487
|
+
const rows = [];
|
|
3488
|
+
const seen = new Set();
|
|
3489
|
+
const addRoute = (routeLike = {}) => {
|
|
3490
|
+
const provider = clean(routeLike.provider);
|
|
3491
|
+
const model = clean(routeLike.model);
|
|
3492
|
+
if (!provider || !model) return;
|
|
3493
|
+
const key = `${provider}:${model}`;
|
|
3494
|
+
if (seen.has(key)) return;
|
|
3495
|
+
seen.add(key);
|
|
3496
|
+
const row = providerModelCacheRow(provider, {
|
|
3497
|
+
id: model,
|
|
3498
|
+
name: routeLike.modelDisplay || routeLike.display || model,
|
|
3499
|
+
display: routeLike.modelDisplay || routeLike.display || model,
|
|
3500
|
+
latest: routeLike.latest === true,
|
|
3501
|
+
supportsReasoning: !!routeLike.effort,
|
|
3502
|
+
mode: 'chat',
|
|
3503
|
+
});
|
|
3504
|
+
rows.push(row);
|
|
3505
|
+
modelMetaByRoute.set(modelMetaKey(provider, model), row);
|
|
3506
|
+
};
|
|
3507
|
+
|
|
3508
|
+
addRoute(route);
|
|
3509
|
+
for (const preset of pickerConfig.presets || []) addRoute(preset);
|
|
3510
|
+
for (const workflowRoute of Object.values(pickerConfig.workflowRoutes || {})) addRoute(workflowRoute);
|
|
3511
|
+
for (const agentRoute of Object.values(pickerConfig.agents || {})) addRoute(agentRoute);
|
|
3512
|
+
return providerModelsFromCacheRows(rows);
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
function addQuickSearchModel(rows, seen, provider, model) {
|
|
3516
|
+
const providerName = normalizeSearchProviderId(provider);
|
|
3517
|
+
const modelId = clean(model?.id || model);
|
|
3518
|
+
if (!providerName || !modelId || !isSearchCapableProvider(providerName)) return;
|
|
3519
|
+
const key = `${providerName}:${modelId}`;
|
|
3520
|
+
if (seen.has(key)) return;
|
|
3521
|
+
const row = providerModelCacheRow(providerName, {
|
|
3522
|
+
id: modelId,
|
|
3523
|
+
name: model?.name || model?.display || modelId,
|
|
3524
|
+
display: model?.display || model?.name || modelId,
|
|
3525
|
+
contextWindow: model?.contextWindow || null,
|
|
3526
|
+
outputTokens: model?.outputTokens || null,
|
|
3527
|
+
latest: model?.latest === true,
|
|
3528
|
+
supportsWebSearch: true,
|
|
3529
|
+
supportsFunctionCalling: model?.supportsFunctionCalling === true,
|
|
3530
|
+
supportsPromptCaching: model?.supportsPromptCaching === true,
|
|
3531
|
+
supportsReasoning: model?.supportsReasoning === true,
|
|
3532
|
+
reasoningLevels: Array.isArray(model?.reasoningLevels) ? model.reasoningLevels : undefined,
|
|
3533
|
+
reasoningOptions: Array.isArray(model?.reasoningOptions) ? model.reasoningOptions : [],
|
|
3534
|
+
mode: 'chat',
|
|
3535
|
+
});
|
|
3536
|
+
if (row.supportsWebSearch !== true) return;
|
|
3537
|
+
seen.add(key);
|
|
3538
|
+
rows.push({
|
|
3539
|
+
...row,
|
|
3540
|
+
provider: providerName,
|
|
3541
|
+
searchCapable: true,
|
|
3542
|
+
searchToolType: row.searchToolType || 'web_search',
|
|
3543
|
+
});
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
function addDefaultSearchModel(rows, seen = new Set()) {
|
|
3547
|
+
const mainModel = currentMainSearchModelMeta();
|
|
3548
|
+
if (!mainModel || !searchCapableFor(route.provider, mainModel)) return;
|
|
3549
|
+
const key = `${SEARCH_DEFAULT_PROVIDER}:${SEARCH_DEFAULT_MODEL}`;
|
|
3550
|
+
if (seen.has(key)) return;
|
|
3551
|
+
seen.add(key);
|
|
3552
|
+
rows.push({
|
|
3553
|
+
id: SEARCH_DEFAULT_MODEL,
|
|
3554
|
+
provider: SEARCH_DEFAULT_PROVIDER,
|
|
3555
|
+
display: 'Default',
|
|
3556
|
+
name: 'Default',
|
|
3557
|
+
description: `Use current main model: ${route.provider}/${route.model}`,
|
|
3558
|
+
supportsWebSearch: true,
|
|
3559
|
+
searchCapable: true,
|
|
3560
|
+
searchToolType: 'web_search',
|
|
3561
|
+
mode: 'chat',
|
|
3562
|
+
});
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
function quickSearchProviderModelRows() {
|
|
3566
|
+
const pickerConfig = displayConfig();
|
|
3567
|
+
const rows = [];
|
|
3568
|
+
const seen = new Set();
|
|
3569
|
+
addDefaultSearchModel(rows, seen);
|
|
3570
|
+
for (const [name, providerConfig] of Object.entries(pickerConfig.providers || {})) {
|
|
3571
|
+
const providerName = normalizeSearchProviderId(name);
|
|
3572
|
+
if (!providerConfig?.enabled || !isSearchCapableProvider(providerName)) continue;
|
|
3573
|
+
for (const model of QUICK_SEARCH_MODELS[providerName] || []) {
|
|
3574
|
+
addQuickSearchModel(rows, seen, providerName, model);
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
const configuredSearch = normalizeSearchRouteConfig(pickerConfig.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
3578
|
+
if (configuredSearch?.provider && configuredSearch?.model) {
|
|
3579
|
+
addQuickSearchModel(rows, seen, configuredSearch.provider, {
|
|
3580
|
+
id: configuredSearch.model,
|
|
3581
|
+
display: configuredSearch.model,
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
const mainModel = currentMainSearchModelMeta();
|
|
3585
|
+
if (mainModel && searchCapableFor(route.provider, mainModel)) {
|
|
3586
|
+
addQuickSearchModel(rows, seen, route.provider, {
|
|
3587
|
+
id: route.model,
|
|
3588
|
+
display: route.model,
|
|
3589
|
+
});
|
|
3590
|
+
}
|
|
3591
|
+
return searchModelsFromRows(rows);
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
function searchModelsFromRows(rows) {
|
|
3595
|
+
return sortProviderModels((rows || [])
|
|
3596
|
+
.filter((row) => row.supportsWebSearch === true)
|
|
3597
|
+
.map((row) => ({
|
|
3598
|
+
...row,
|
|
3599
|
+
provider: normalizeSearchProviderId(row.provider),
|
|
3600
|
+
searchCapable: true,
|
|
3601
|
+
searchToolType: row.searchToolType || 'web_search',
|
|
3602
|
+
})));
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
function searchRowsWithDefault(rows = []) {
|
|
3606
|
+
const out = [];
|
|
3607
|
+
const seen = new Set();
|
|
3608
|
+
addDefaultSearchModel(out, seen);
|
|
3609
|
+
for (const row of rows || []) {
|
|
3610
|
+
const providerName = normalizeSearchProviderId(row?.provider);
|
|
3611
|
+
const modelId = clean(row?.id || row?.model);
|
|
3612
|
+
if (providerName === SEARCH_DEFAULT_PROVIDER && modelId.toLowerCase() === SEARCH_DEFAULT_MODEL) continue;
|
|
3613
|
+
const key = `${providerName}:${modelId}`;
|
|
3614
|
+
if (!providerName || !modelId || seen.has(key)) continue;
|
|
3615
|
+
seen.add(key);
|
|
3616
|
+
out.push(row);
|
|
3617
|
+
}
|
|
3618
|
+
return out;
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
async function collectSearchProviderModels({ force = false } = {}) {
|
|
3622
|
+
if (!force && Array.isArray(searchProviderModelsCache.models)) {
|
|
3623
|
+
return providerModelsFromCacheRows(searchRowsWithDefault(searchProviderModelsCache.models));
|
|
3624
|
+
}
|
|
3625
|
+
if (!force && Array.isArray(providerModelsCache.models)) {
|
|
3626
|
+
const rows = searchRowsWithDefault(searchModelsFromRows(providerModelsCache.models));
|
|
3627
|
+
searchProviderModelsCache = { models: rows, at: Date.now() };
|
|
3628
|
+
return providerModelsFromCacheRows(rows);
|
|
3629
|
+
}
|
|
3630
|
+
if (!force) {
|
|
3631
|
+
const rows = searchRowsWithDefault(quickSearchProviderModelRows());
|
|
3632
|
+
searchProviderModelsCache = { models: rows, at: Date.now() };
|
|
3633
|
+
return providerModelsFromCacheRows(rows);
|
|
3634
|
+
}
|
|
3635
|
+
if (!searchProviderModelsPromise) {
|
|
3636
|
+
searchProviderModelsPromise = loadSearchProviderModelsFresh()
|
|
3637
|
+
.then((models) => {
|
|
3638
|
+
searchProviderModelsCache = { models, at: Date.now() };
|
|
3639
|
+
return models;
|
|
3640
|
+
})
|
|
3641
|
+
.finally(() => {
|
|
3642
|
+
searchProviderModelsPromise = null;
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
return providerModelsFromCacheRows(await searchProviderModelsPromise);
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
function enabledSearchProviderConfig() {
|
|
2398
3649
|
ensureFullConfig();
|
|
3650
|
+
const out = {};
|
|
3651
|
+
for (const [name, providerConfig] of Object.entries(config.providers || {})) {
|
|
3652
|
+
const providerName = normalizeSearchProviderId(name);
|
|
3653
|
+
if (!providerConfig?.enabled || !isSearchCapableProvider(providerName)) continue;
|
|
3654
|
+
out[providerName] = { ...providerConfig, enabled: true };
|
|
3655
|
+
}
|
|
3656
|
+
return out;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
async function loadSearchProviderModelsFresh() {
|
|
3660
|
+
const searchProviders = enabledSearchProviderConfig();
|
|
3661
|
+
const providerNames = Object.keys(searchProviders);
|
|
3662
|
+
if (!providerNames.length) return [];
|
|
2399
3663
|
await ensureProvidersReady(config.providers || {});
|
|
2400
|
-
const
|
|
3664
|
+
const providerResults = await Promise.all(providerNames.map(async (name) => {
|
|
3665
|
+
const provider = reg.getProvider(name);
|
|
3666
|
+
if (typeof provider?.listModels !== 'function') return [];
|
|
3667
|
+
try {
|
|
3668
|
+
const models = await provider.listModels();
|
|
3669
|
+
if (!Array.isArray(models)) return [];
|
|
3670
|
+
const rows = [];
|
|
3671
|
+
for (const m of models) {
|
|
3672
|
+
if (!m?.id || !isSelectableLlmModel(m)) continue;
|
|
3673
|
+
const row = providerModelCacheRow(name, m);
|
|
3674
|
+
if (row.supportsWebSearch !== true) continue;
|
|
3675
|
+
rows.push({
|
|
3676
|
+
...row,
|
|
3677
|
+
provider: normalizeSearchProviderId(row.provider),
|
|
3678
|
+
searchCapable: true,
|
|
3679
|
+
searchToolType: row.searchToolType || 'web_search',
|
|
3680
|
+
});
|
|
3681
|
+
modelMetaByRoute.set(modelMetaKey(name, m.id), row);
|
|
3682
|
+
}
|
|
3683
|
+
return rows;
|
|
3684
|
+
} catch {
|
|
3685
|
+
// Keep the picker responsive if one search-capable provider has a
|
|
3686
|
+
// transient catalog/auth failure.
|
|
3687
|
+
return [];
|
|
3688
|
+
}
|
|
3689
|
+
}));
|
|
2401
3690
|
const results = [];
|
|
2402
3691
|
const seen = new Set();
|
|
2403
|
-
|
|
2404
|
-
|
|
3692
|
+
addDefaultSearchModel(results, seen);
|
|
3693
|
+
for (const row of providerResults.flat()) {
|
|
3694
|
+
const key = `${normalizeSearchProviderId(row.provider)}:${row.id}`;
|
|
3695
|
+
if (seen.has(key)) continue;
|
|
3696
|
+
seen.add(key);
|
|
3697
|
+
results.push(row);
|
|
3698
|
+
}
|
|
3699
|
+
return results;
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
async function loadProviderModelsFresh() {
|
|
3703
|
+
ensureFullConfig();
|
|
3704
|
+
await ensureProvidersReady(config.providers || {});
|
|
3705
|
+
const allProviders = [...reg.getAllProviders()];
|
|
3706
|
+
const providerResults = await Promise.all(allProviders.map(async ([name, provider]) => {
|
|
3707
|
+
if (typeof provider?.listModels !== 'function') return [];
|
|
2405
3708
|
try {
|
|
2406
3709
|
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
|
-
}
|
|
3710
|
+
if (!Array.isArray(models)) return [];
|
|
3711
|
+
const rows = [];
|
|
3712
|
+
for (const m of models) {
|
|
3713
|
+
if (!m?.id) continue;
|
|
3714
|
+
if (!isSelectableLlmModel(m)) continue;
|
|
3715
|
+
rows.push(providerModelCacheRow(name, m));
|
|
2418
3716
|
}
|
|
3717
|
+
return rows;
|
|
2419
3718
|
} catch {
|
|
2420
3719
|
// Ignore per-provider catalog failures so one bad credential or
|
|
2421
3720
|
// transient /models error does not hide other authenticated models.
|
|
3721
|
+
return [];
|
|
2422
3722
|
}
|
|
3723
|
+
}));
|
|
3724
|
+
const results = [];
|
|
3725
|
+
const seen = new Set();
|
|
3726
|
+
for (const row of providerResults.flat()) {
|
|
3727
|
+
const key = `${row.provider}:${row.id}`;
|
|
3728
|
+
if (seen.has(key)) continue;
|
|
3729
|
+
seen.add(key);
|
|
3730
|
+
results.push(row);
|
|
3731
|
+
modelMetaByRoute.set(modelMetaKey(row.provider, row.id), row);
|
|
2423
3732
|
}
|
|
2424
3733
|
return results;
|
|
2425
3734
|
}
|
|
2426
3735
|
|
|
2427
|
-
async function collectProviderModels({ force = false } = {}) {
|
|
3736
|
+
async function collectProviderModels({ force = false, quick = false } = {}) {
|
|
2428
3737
|
if (!force && Array.isArray(providerModelsCache.models)) {
|
|
2429
3738
|
return providerModelsFromCacheRows(providerModelsCache.models);
|
|
2430
3739
|
}
|
|
3740
|
+
if (!force && quick) {
|
|
3741
|
+
warmProviderModelCache();
|
|
3742
|
+
return quickProviderModelRows();
|
|
3743
|
+
}
|
|
2431
3744
|
if (!providerModelsPromise) {
|
|
2432
3745
|
providerModelsPromise = loadProviderModelsFresh()
|
|
2433
3746
|
.then((models) => {
|
|
@@ -2465,7 +3778,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2465
3778
|
const models = await collectProviderModels();
|
|
2466
3779
|
const picked = models[0] || null;
|
|
2467
3780
|
if (!picked) {
|
|
2468
|
-
throw new Error('No provider models available.
|
|
3781
|
+
throw new Error('No provider models available. Open /providers to sign in, then /model to choose a model.');
|
|
2469
3782
|
}
|
|
2470
3783
|
route = {
|
|
2471
3784
|
...route,
|
|
@@ -2498,7 +3811,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2498
3811
|
|
|
2499
3812
|
function requireModelRoute() {
|
|
2500
3813
|
if (routeHasModel()) return;
|
|
2501
|
-
throw new Error('No model configured.
|
|
3814
|
+
throw new Error('No model configured. Open /providers to sign in, then /model to choose a model.');
|
|
2502
3815
|
}
|
|
2503
3816
|
|
|
2504
3817
|
async function recreateCurrentSessionIfReady() {
|
|
@@ -2537,8 +3850,8 @@ function parsedProviderModelVersion(id) {
|
|
|
2537
3850
|
const coreMemoryContext = await loadCoreMemoryContext();
|
|
2538
3851
|
if (closeRequested) throw new Error('runtime is closing');
|
|
2539
3852
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
3853
|
+
const workflow = activeWorkflowSummary(config, dataDir);
|
|
2540
3854
|
const workflowContext = workflowContextBlock(config, dataDir);
|
|
2541
|
-
const workspaceContext = buildWorkspaceContext();
|
|
2542
3855
|
const sessionOpts = {
|
|
2543
3856
|
provider: route.provider,
|
|
2544
3857
|
model: route.model,
|
|
@@ -2553,9 +3866,12 @@ function parsedProviderModelVersion(id) {
|
|
|
2553
3866
|
disallowedTools: LEAD_DISALLOWED_TOOLS,
|
|
2554
3867
|
cwd: currentCwd,
|
|
2555
3868
|
coreMemoryContext,
|
|
3869
|
+
workflow,
|
|
2556
3870
|
workflowContext,
|
|
2557
|
-
workspaceContext,
|
|
2558
3871
|
fast: route.fast === true,
|
|
3872
|
+
compaction: config.compaction && typeof config.compaction === 'object'
|
|
3873
|
+
? normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() })
|
|
3874
|
+
: undefined,
|
|
2559
3875
|
};
|
|
2560
3876
|
if (hasOwn(route, 'effort') || route.effectiveEffort) {
|
|
2561
3877
|
sessionOpts.effort = route.effectiveEffort || null;
|
|
@@ -2563,15 +3879,55 @@ function parsedProviderModelVersion(id) {
|
|
|
2563
3879
|
session = mgr.createSession(sessionOpts);
|
|
2564
3880
|
sessionNeedsCwdRefresh = false;
|
|
2565
3881
|
Object.defineProperty(session, 'beforeToolHook', {
|
|
2566
|
-
value: (input) => hooks.beforeTool(
|
|
3882
|
+
value: (input) => hooks.beforeTool(hookCommonPayload({
|
|
3883
|
+
...input,
|
|
3884
|
+
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
3885
|
+
tool_name: input?.name || input?.tool_name,
|
|
3886
|
+
tool_input: input?.args || input?.tool_input,
|
|
3887
|
+
tool_use_id: input?.toolCallId || input?.tool_use_id,
|
|
3888
|
+
cwd: input?.cwd || currentCwd,
|
|
3889
|
+
})),
|
|
3890
|
+
enumerable: false,
|
|
3891
|
+
configurable: true,
|
|
3892
|
+
writable: true,
|
|
3893
|
+
});
|
|
3894
|
+
// PostToolUse: bridge runtime tool completions to the standard hook bus.
|
|
3895
|
+
// dispatch() returns a promise; the loop's afterToolHook caller already
|
|
3896
|
+
// try/catches, so a rejection cannot escape the tool loop.
|
|
3897
|
+
Object.defineProperty(session, 'afterToolHook', {
|
|
3898
|
+
value: (input) => hooks.dispatch('PostToolUse', hookCommonPayload({
|
|
3899
|
+
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
3900
|
+
cwd: input?.cwd || currentCwd,
|
|
3901
|
+
tool_name: input?.name,
|
|
3902
|
+
tool_input: input?.args,
|
|
3903
|
+
tool_use_id: input?.toolCallId || input?.tool_use_id,
|
|
3904
|
+
tool_response: input?.result,
|
|
3905
|
+
})),
|
|
2567
3906
|
enumerable: false,
|
|
2568
3907
|
configurable: true,
|
|
2569
3908
|
writable: true,
|
|
2570
3909
|
});
|
|
2571
|
-
applyDeferredToolSurface(session, mode, standaloneTools);
|
|
3910
|
+
applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
2572
3911
|
applyPreSessionToolSelection();
|
|
2573
|
-
statusRoutes
|
|
3912
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
2574
3913
|
hooks.emit('session:create', { sessionId: session.id, provider: route.provider, model: route.model, toolMode: mode, cwd: currentCwd });
|
|
3914
|
+
// SessionStart: bridge to the standard Claude Code hook bus. Best-effort;
|
|
3915
|
+
// a hook error must never break session creation. additionalContext is
|
|
3916
|
+
// injected before the first user turn as a system-reminder context pair.
|
|
3917
|
+
try {
|
|
3918
|
+
const startSource = /resume/i.test(String(reason || ''))
|
|
3919
|
+
? 'resume'
|
|
3920
|
+
: (/clear/i.test(String(reason || '')) ? 'clear' : 'startup');
|
|
3921
|
+
const startDispatch = await hooks.dispatch('SessionStart', hookCommonPayload({ session_id: session.id, source: startSource, model: route.model }));
|
|
3922
|
+
const startContext = Array.isArray(startDispatch?.additionalContext)
|
|
3923
|
+
? startDispatch.additionalContext.join('\n\n')
|
|
3924
|
+
: String(startDispatch?.additionalContext || '');
|
|
3925
|
+
if (startContext.trim()) {
|
|
3926
|
+
session.messages.push({ role: 'user', content: `<system-reminder>\n# SessionStart Hook Context\n${startContext.trim()}\n</system-reminder>` });
|
|
3927
|
+
session.messages.push({ role: 'assistant', content: '.' });
|
|
3928
|
+
session.updatedAt = Date.now();
|
|
3929
|
+
}
|
|
3930
|
+
} catch { /* best-effort: never break session create */ }
|
|
2575
3931
|
bootProfile('session:create:ready', {
|
|
2576
3932
|
ms: (performance.now() - startedAt).toFixed(1),
|
|
2577
3933
|
reason,
|
|
@@ -2590,7 +3946,10 @@ function parsedProviderModelVersion(id) {
|
|
|
2590
3946
|
}
|
|
2591
3947
|
|
|
2592
3948
|
function scheduleLeadSessionPrewarm() {
|
|
2593
|
-
if (
|
|
3949
|
+
if (!sessionPrewarmEnabled) {
|
|
3950
|
+
bootProfile('session:prewarm-skipped');
|
|
3951
|
+
return;
|
|
3952
|
+
}
|
|
2594
3953
|
const timer = setTimeout(() => {
|
|
2595
3954
|
if (closeRequested || session?.id || sessionCreatePromise || activeTurnCount > 0) return;
|
|
2596
3955
|
void createCurrentSession('prewarm')
|
|
@@ -2601,7 +3960,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2601
3960
|
}
|
|
2602
3961
|
|
|
2603
3962
|
function scheduleProviderWarmup(delayMs = providerWarmupDelayMs) {
|
|
2604
|
-
if (
|
|
3963
|
+
if (!providerWarmupEnabled) {
|
|
2605
3964
|
bootProfile('providers:warm-skipped');
|
|
2606
3965
|
return;
|
|
2607
3966
|
}
|
|
@@ -2609,6 +3968,10 @@ function parsedProviderModelVersion(id) {
|
|
|
2609
3968
|
providerWarmupTimer = setTimeout(() => {
|
|
2610
3969
|
providerWarmupTimer = null;
|
|
2611
3970
|
if (closeRequested) return;
|
|
3971
|
+
if (!firstTurnCompleted && !envFlag('MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN')) {
|
|
3972
|
+
bootProfile('providers:warm-deferred', { reason: 'first-turn-pending' });
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
2612
3975
|
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
2613
3976
|
bootProfile('providers:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
2614
3977
|
scheduleProviderWarmup(backgroundBusyRetryMs);
|
|
@@ -2616,8 +3979,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2616
3979
|
}
|
|
2617
3980
|
const providersStartedAt = performance.now();
|
|
2618
3981
|
try {
|
|
2619
|
-
|
|
2620
|
-
configHasSecrets = true;
|
|
3982
|
+
reloadFullConfig();
|
|
2621
3983
|
} catch (error) {
|
|
2622
3984
|
bootProfile('config:full-failed', { error: error?.message || String(error) });
|
|
2623
3985
|
}
|
|
@@ -2625,28 +3987,27 @@ function parsedProviderModelVersion(id) {
|
|
|
2625
3987
|
.then(() => {
|
|
2626
3988
|
bootProfile('providers:init:ready', { ms: (performance.now() - providersStartedAt).toFixed(1) });
|
|
2627
3989
|
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
|
-
});
|
|
3990
|
+
return true;
|
|
2642
3991
|
})
|
|
2643
3992
|
.catch((error) => bootProfile('providers:warm-failed', { error: error?.message || String(error) }));
|
|
2644
3993
|
}, delayMs);
|
|
2645
3994
|
providerWarmupTimer.unref?.();
|
|
2646
3995
|
}
|
|
2647
3996
|
|
|
3997
|
+
function scheduleProviderSetupWarmup(delayMs = providerSetupWarmupDelayMs) {
|
|
3998
|
+
if (providerSetupWarmupTimer || closeRequested) return;
|
|
3999
|
+
providerSetupWarmupTimer = setTimeout(() => {
|
|
4000
|
+
providerSetupWarmupTimer = null;
|
|
4001
|
+
if (closeRequested) return;
|
|
4002
|
+
void cachedProviderSetup()
|
|
4003
|
+
.then(() => bootProfile('provider-setup:warm-ready'))
|
|
4004
|
+
.catch((error) => bootProfile('provider-setup:warm-failed', { error: error?.message || String(error) }));
|
|
4005
|
+
}, delayMs);
|
|
4006
|
+
providerSetupWarmupTimer.unref?.();
|
|
4007
|
+
}
|
|
4008
|
+
|
|
2648
4009
|
function scheduleProviderModelWarmup(delayMs = providerModelWarmupDelayMs) {
|
|
2649
|
-
if (
|
|
4010
|
+
if (!modelPrefetchEnabled) return;
|
|
2650
4011
|
if (providerModelWarmupTimer || closeRequested) return;
|
|
2651
4012
|
providerModelWarmupTimer = setTimeout(() => {
|
|
2652
4013
|
providerModelWarmupTimer = null;
|
|
@@ -2661,11 +4022,75 @@ function parsedProviderModelVersion(id) {
|
|
|
2661
4022
|
providerModelWarmupTimer.unref?.();
|
|
2662
4023
|
}
|
|
2663
4024
|
|
|
4025
|
+
function scheduleStatuslineUsageWarmup(delayMs = statuslineUsageWarmupDelayMs) {
|
|
4026
|
+
const providerId = clean(route?.provider);
|
|
4027
|
+
if (!providerId || !providerId.includes('oauth')) {
|
|
4028
|
+
bootProfile('statusline-usage:warm-skipped', { provider: providerId || null });
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
if (statuslineUsageWarmupTimer || closeRequested) return;
|
|
4032
|
+
statuslineUsageWarmupTimer = setTimeout(async () => {
|
|
4033
|
+
statuslineUsageWarmupTimer = null;
|
|
4034
|
+
if (closeRequested) return;
|
|
4035
|
+
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
4036
|
+
bootProfile('statusline-usage:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
4037
|
+
scheduleStatuslineUsageWarmup(backgroundBusyRetryMs);
|
|
4038
|
+
return;
|
|
4039
|
+
}
|
|
4040
|
+
try {
|
|
4041
|
+
ensureConfigForRouteProvider();
|
|
4042
|
+
await ensureProvidersReady(ensureProviderEnabled(config, route.provider));
|
|
4043
|
+
if (closeRequested) return;
|
|
4044
|
+
refreshStatuslineUsageSnapshot(route);
|
|
4045
|
+
bootProfile('statusline-usage:warm-ready', { provider: clean(route?.provider) });
|
|
4046
|
+
} catch (error) {
|
|
4047
|
+
bootProfile('statusline-usage:warm-failed', { error: error?.message || String(error) });
|
|
4048
|
+
} finally {
|
|
4049
|
+
scheduleStatuslineUsageRefresh();
|
|
4050
|
+
}
|
|
4051
|
+
}, delayMs);
|
|
4052
|
+
statuslineUsageWarmupTimer.unref?.();
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
// Idle keep-alive loop: periodically re-fetch the OAuth usage snapshot so its
|
|
4056
|
+
// cachedAt stays "live-fresh" and the statusline usage segment does not vanish
|
|
4057
|
+
// after LIVE_USAGE_SNAPSHOT_MAX_AGE_MS while the session is idle. Turn-driven
|
|
4058
|
+
// refreshes (recordStandaloneStatusTelemetry) already cover active sessions.
|
|
4059
|
+
function scheduleStatuslineUsageRefresh(delayMs = statuslineUsageRefreshDelayMs) {
|
|
4060
|
+
const providerId = clean(route?.provider);
|
|
4061
|
+
if (!providerId || !providerId.includes('oauth')) return;
|
|
4062
|
+
if (statuslineUsageRefreshTimer || closeRequested) return;
|
|
4063
|
+
statuslineUsageRefreshTimer = setTimeout(async () => {
|
|
4064
|
+
statuslineUsageRefreshTimer = null;
|
|
4065
|
+
if (closeRequested) return;
|
|
4066
|
+
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
4067
|
+
// Active turns refresh usage on their own; just re-arm the idle loop.
|
|
4068
|
+
scheduleStatuslineUsageRefresh();
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
try {
|
|
4072
|
+
ensureConfigForRouteProvider();
|
|
4073
|
+
await ensureProvidersReady(ensureProviderEnabled(config, route.provider));
|
|
4074
|
+
if (closeRequested) return;
|
|
4075
|
+
refreshStatuslineUsageSnapshot(route);
|
|
4076
|
+
} catch {
|
|
4077
|
+
// Usage display must never affect the session runtime.
|
|
4078
|
+
} finally {
|
|
4079
|
+
scheduleStatuslineUsageRefresh();
|
|
4080
|
+
}
|
|
4081
|
+
}, delayMs);
|
|
4082
|
+
statuslineUsageRefreshTimer.unref?.();
|
|
4083
|
+
}
|
|
4084
|
+
|
|
2664
4085
|
function scheduleChannelStart(delayMs = channelStartDelayMs) {
|
|
2665
4086
|
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
2666
4087
|
bootProfile('channels:start-skipped');
|
|
2667
4088
|
return;
|
|
2668
4089
|
}
|
|
4090
|
+
if (!channelsEnabled()) {
|
|
4091
|
+
bootProfile('channels:start-disabled');
|
|
4092
|
+
return;
|
|
4093
|
+
}
|
|
2669
4094
|
if (channelStartTimer || closeRequested) return;
|
|
2670
4095
|
bootProfile('channels:start-scheduled', { delayMs });
|
|
2671
4096
|
channelStartTimer = setTimeout(() => {
|
|
@@ -2700,12 +4125,31 @@ function parsedProviderModelVersion(id) {
|
|
|
2700
4125
|
});
|
|
2701
4126
|
}
|
|
2702
4127
|
|
|
2703
|
-
bootProfile('session-runtime:ready', {
|
|
4128
|
+
bootProfile('session-runtime:ready', {
|
|
4129
|
+
lazySession: true,
|
|
4130
|
+
prewarmSession: sessionPrewarmEnabled,
|
|
4131
|
+
providerWarmup: providerWarmupEnabled,
|
|
4132
|
+
codeGraphPrewarm: codeGraphPrewarmEnabled,
|
|
4133
|
+
});
|
|
2704
4134
|
scheduleLeadSessionPrewarm();
|
|
4135
|
+
// Lazy mode (default): skip the startup prewarm entirely; the first turn
|
|
4136
|
+
// triggers it instead (see ask()). Eager mode keeps the old startup schedule.
|
|
4137
|
+
if (!codeGraphPrewarmLazy) {
|
|
4138
|
+
scheduleCodeGraphPrewarm(codeGraphPrewarmDelayMs, 'startup');
|
|
4139
|
+
} else {
|
|
4140
|
+
bootProfile('code-graph:prewarm-lazy', { reason: 'startup-deferred-to-first-turn' });
|
|
4141
|
+
}
|
|
4142
|
+
scheduleProviderSetupWarmup();
|
|
2705
4143
|
scheduleProviderWarmup();
|
|
4144
|
+
// Warm the provider model catalog in the background, but keep it on its own
|
|
4145
|
+
// delay so short-lived detached runtimes can exit before /models I/O starts.
|
|
4146
|
+
// Operators that want earlier catalog warming can lower
|
|
4147
|
+
// MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS explicitly.
|
|
4148
|
+
scheduleProviderModelWarmup();
|
|
4149
|
+
scheduleStatuslineUsageWarmup();
|
|
2706
4150
|
scheduleChannelStart();
|
|
2707
4151
|
|
|
2708
|
-
function contextStatusCacheKeyFor({ messages, tools
|
|
4152
|
+
function contextStatusCacheKeyFor({ messages, tools }) {
|
|
2709
4153
|
const compaction = session?.compaction || {};
|
|
2710
4154
|
const lastMessage = messages[messages.length - 1] || null;
|
|
2711
4155
|
return {
|
|
@@ -2715,7 +4159,6 @@ function parsedProviderModelVersion(id) {
|
|
|
2715
4159
|
model: route.model,
|
|
2716
4160
|
cwd: currentCwd,
|
|
2717
4161
|
mode,
|
|
2718
|
-
bridgeMode,
|
|
2719
4162
|
messages,
|
|
2720
4163
|
messageCount: messages.length,
|
|
2721
4164
|
lastMessage,
|
|
@@ -2788,23 +4231,25 @@ function parsedProviderModelVersion(id) {
|
|
|
2788
4231
|
get toolMode() {
|
|
2789
4232
|
return mode;
|
|
2790
4233
|
},
|
|
2791
|
-
get bridgeMode() {
|
|
2792
|
-
return bridge.getDefaultMode();
|
|
2793
|
-
},
|
|
2794
4234
|
get autoClear() {
|
|
2795
4235
|
return normalizeAutoClearConfig(config.autoClear);
|
|
2796
4236
|
},
|
|
2797
4237
|
get systemShell() {
|
|
2798
4238
|
return normalizeSystemShellConfig(config.shell);
|
|
2799
4239
|
},
|
|
4240
|
+
get searchRoute() {
|
|
4241
|
+
searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
4242
|
+
return searchRoute;
|
|
4243
|
+
},
|
|
2800
4244
|
get workflow() {
|
|
2801
4245
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
2802
|
-
|
|
2803
|
-
|
|
4246
|
+
if (session?.workflow && typeof session.workflow === 'object') {
|
|
4247
|
+
return workflowSummary(session.workflow);
|
|
4248
|
+
}
|
|
4249
|
+
return activeWorkflowSummary(config, dataDir);
|
|
2804
4250
|
},
|
|
2805
4251
|
get outputStyle() {
|
|
2806
|
-
|
|
2807
|
-
return outputStyleStatus(dataDir).current;
|
|
4252
|
+
return getOutputStyleStatusCached().current;
|
|
2808
4253
|
},
|
|
2809
4254
|
get cwd() {
|
|
2810
4255
|
return currentCwd;
|
|
@@ -2813,10 +4258,16 @@ function parsedProviderModelVersion(id) {
|
|
|
2813
4258
|
return session;
|
|
2814
4259
|
},
|
|
2815
4260
|
contextStatus() {
|
|
2816
|
-
|
|
4261
|
+
// Prefer the in-flight working transcript while a turn is running so the
|
|
4262
|
+
// context gauge reflects LIVE growth (user turn + tool calls/results) as
|
|
4263
|
+
// it accumulates, instead of freezing at the pre-turn committed snapshot.
|
|
4264
|
+
// askSession() sets session.liveTurnMessages for the turn duration and
|
|
4265
|
+
// clears it on commit/cancel/error, after which we fall back to the
|
|
4266
|
+
// authoritative committed transcript.
|
|
4267
|
+
const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
|
|
4268
|
+
const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
|
|
2817
4269
|
const tools = Array.isArray(session?.tools) ? session.tools : [];
|
|
2818
|
-
const
|
|
2819
|
-
const cacheKey = contextStatusCacheKeyFor({ messages, tools, bridgeMode });
|
|
4270
|
+
const cacheKey = contextStatusCacheKeyFor({ messages, tools });
|
|
2820
4271
|
if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
|
|
2821
4272
|
return contextStatusCacheValue;
|
|
2822
4273
|
}
|
|
@@ -2829,7 +4280,9 @@ function parsedProviderModelVersion(id) {
|
|
|
2829
4280
|
const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
|
|
2830
4281
|
const effectiveWindow = Number(session?.contextWindow || rawWindow || 0);
|
|
2831
4282
|
const lastContextTokens = Number(session?.lastContextTokens || 0);
|
|
2832
|
-
const estimatedContextTokens =
|
|
4283
|
+
const estimatedContextTokens = estimateTranscriptContextUsage(messages, tools, {
|
|
4284
|
+
messageCount: messageSummary.count,
|
|
4285
|
+
});
|
|
2833
4286
|
const compactAt = Number(session?.compaction?.lastChangedAt || session?.compaction?.lastCompactAt || 0);
|
|
2834
4287
|
const usageAt = Number(session?.lastContextTokensUpdatedAt || 0);
|
|
2835
4288
|
const lastUsageStale = !!lastContextTokens && (
|
|
@@ -2839,31 +4292,58 @@ function parsedProviderModelVersion(id) {
|
|
|
2839
4292
|
);
|
|
2840
4293
|
const compactBoundaryTokens = Number(session?.compactBoundaryTokens || session?.compaction?.boundaryTokens || 0);
|
|
2841
4294
|
const displayWindow = compactBoundaryTokens || effectiveWindow;
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
4295
|
+
// The transcript estimate is the single source of truth for the displayed
|
|
4296
|
+
// context footprint. Provider-reported input_tokens (lastContextTokens)
|
|
4297
|
+
// swing non-monotonically and are not window-bounded on some providers
|
|
4298
|
+
// (e.g. OpenAI gpt-5.5 Responses API), so they are kept only as secondary
|
|
4299
|
+
// metadata (lastApiRequestTokens / usage.lastContextTokens) and never feed
|
|
4300
|
+
// the gauge numerator.
|
|
4301
|
+
const usedTokens = estimatedContextTokens;
|
|
2845
4302
|
const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
|
|
2846
4303
|
const autoCompactTokenLimit = Number(session?.autoCompactTokenLimit || 0);
|
|
2847
|
-
|
|
2848
|
-
|
|
4304
|
+
// The runtime fires auto-compaction at the effective boundary by default.
|
|
4305
|
+
// Compute the same trigger here so /context shows what will actually fire
|
|
4306
|
+
// before any compaction telemetry (session.compaction.triggerTokens) is
|
|
4307
|
+
// recorded. Explicit buffer settings can still lower the trigger.
|
|
4308
|
+
// An explicit autoCompactTokenLimit (provider/catalog/seed supplied)
|
|
4309
|
+
// still takes precedence and is honored verbatim when it sits at or below
|
|
4310
|
+
// the boundary.
|
|
4311
|
+
const defaultCompactBufferTokens = contextDefaultBufferTokens(compactBoundaryTokens, session?.compaction || {});
|
|
4312
|
+
const defaultCompactTriggerTokens = compactBoundaryTokens
|
|
4313
|
+
? Math.max(1, compactBoundaryTokens - defaultCompactBufferTokens)
|
|
4314
|
+
: 0;
|
|
4315
|
+
// Only an explicit auto-compact limit STRICTLY BELOW the boundary is a
|
|
4316
|
+
// real trigger; a persisted value == boundary is a legacy derived
|
|
4317
|
+
// full-window artifact and must fall through to the default trigger.
|
|
4318
|
+
// Likewise ignore persisted default-buffer telemetry so the displayed
|
|
4319
|
+
// trigger matches what the runtime now actually fires.
|
|
4320
|
+
const persistedTriggerTokens = Number(session?.compaction?.triggerTokens || 0);
|
|
4321
|
+
const legacyDefaultTriggerTelemetry = isLegacyDefaultContextBufferTelemetry(session?.compaction || {}, compactBoundaryTokens)
|
|
4322
|
+
&& persistedTriggerTokens > 0
|
|
4323
|
+
&& persistedTriggerTokens < compactBoundaryTokens;
|
|
4324
|
+
const usablePersistedTrigger = persistedTriggerTokens > 0
|
|
4325
|
+
&& !legacyDefaultTriggerTelemetry
|
|
4326
|
+
&& (!compactBoundaryTokens || persistedTriggerTokens < compactBoundaryTokens)
|
|
4327
|
+
? persistedTriggerTokens
|
|
4328
|
+
: 0;
|
|
4329
|
+
const compactTriggerTokens = autoCompactTokenLimit && compactBoundaryTokens && autoCompactTokenLimit < compactBoundaryTokens
|
|
2849
4330
|
? autoCompactTokenLimit
|
|
2850
|
-
:
|
|
2851
|
-
const compactBufferTokens = Number(
|
|
4331
|
+
: (usablePersistedTrigger || defaultCompactTriggerTokens || 0);
|
|
4332
|
+
const compactBufferTokens = Number(compactBoundaryTokens && compactTriggerTokens
|
|
4333
|
+
? Math.max(0, compactBoundaryTokens - compactTriggerTokens)
|
|
4334
|
+
: defaultCompactBufferTokens);
|
|
2852
4335
|
const value = {
|
|
2853
4336
|
sessionId: session?.id || null,
|
|
2854
4337
|
provider: route.provider,
|
|
2855
4338
|
model: route.model,
|
|
2856
4339
|
cwd: currentCwd,
|
|
2857
4340
|
toolMode: mode,
|
|
2858
|
-
bridgeMode,
|
|
2859
4341
|
contextWindow: displayWindow || effectiveWindow || null,
|
|
2860
4342
|
effectiveContextWindow: effectiveWindow || null,
|
|
2861
4343
|
rawContextWindow: rawWindow || null,
|
|
2862
4344
|
effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
|
|
2863
4345
|
usedTokens,
|
|
2864
|
-
usedSource:
|
|
2865
|
-
? 'last_api_request'
|
|
2866
|
-
: 'estimated',
|
|
4346
|
+
usedSource: 'estimated',
|
|
2867
4347
|
currentEstimatedTokens: estimatedContextTokens,
|
|
2868
4348
|
lastApiRequestTokens: lastContextTokens || 0,
|
|
2869
4349
|
lastApiRequestStale: lastUsageStale,
|
|
@@ -2901,26 +4381,64 @@ function parsedProviderModelVersion(id) {
|
|
|
2901
4381
|
return value;
|
|
2902
4382
|
},
|
|
2903
4383
|
listProviders() {
|
|
2904
|
-
return renderProviderStatus(
|
|
4384
|
+
return renderProviderStatus(displayConfig());
|
|
2905
4385
|
},
|
|
2906
4386
|
async getProviderSetup() {
|
|
2907
4387
|
return await cachedProviderSetup();
|
|
2908
4388
|
},
|
|
2909
4389
|
async getUsageDashboard(options = {}) {
|
|
2910
|
-
const
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
4390
|
+
const forceSetup = options?.force === true || options?.refresh === true;
|
|
4391
|
+
if (!forceSetup && usageDashboardCache.dashboard) {
|
|
4392
|
+
const cached = {
|
|
4393
|
+
...usageDashboardCache.dashboard,
|
|
4394
|
+
refresh: false,
|
|
4395
|
+
checking: false,
|
|
4396
|
+
cached: true,
|
|
4397
|
+
cachedAt: usageDashboardCache.at,
|
|
4398
|
+
};
|
|
4399
|
+
if (typeof options?.onUpdate === 'function') {
|
|
4400
|
+
try { options.onUpdate(cached); } catch {}
|
|
4401
|
+
}
|
|
4402
|
+
return cached;
|
|
4403
|
+
}
|
|
4404
|
+
if (!forceSetup && usageDashboardPromise) return await usageDashboardPromise;
|
|
4405
|
+
const quickSetup = options?.quickSetup !== false;
|
|
4406
|
+
const getProvider = (providerId) => reg.getProvider(providerId);
|
|
4407
|
+
const log = (message) => {
|
|
4408
|
+
if (process.env.MIXDOG_USAGE_TRACE) {
|
|
4409
|
+
try { process.stderr.write(`[usage] ${message}\n`); } catch {}
|
|
4410
|
+
}
|
|
4411
|
+
};
|
|
4412
|
+
if (quickSetup && typeof options?.onUpdate === 'function') {
|
|
4413
|
+
const previewConfig = displayConfig();
|
|
4414
|
+
const previewSetup = await cachedProviderSetup({ force: false, quick: true });
|
|
4415
|
+
await createUsageDashboard(previewConfig, {
|
|
4416
|
+
...(options || {}),
|
|
4417
|
+
preview: true,
|
|
4418
|
+
setup: previewSetup,
|
|
4419
|
+
getProvider,
|
|
4420
|
+
log,
|
|
4421
|
+
});
|
|
4422
|
+
}
|
|
4423
|
+
const buildDashboard = async () => {
|
|
4424
|
+
const dashboard = await createUsageDashboard(displayConfig(), {
|
|
4425
|
+
...(options || {}),
|
|
4426
|
+
setup: await cachedProviderSetup({ force: forceSetup, quick: false }),
|
|
4427
|
+
getProvider,
|
|
4428
|
+
log,
|
|
4429
|
+
});
|
|
4430
|
+
usageDashboardCache = { dashboard, at: Date.now() };
|
|
4431
|
+
return dashboard;
|
|
4432
|
+
};
|
|
4433
|
+
if (forceSetup) return await buildDashboard();
|
|
4434
|
+
usageDashboardPromise = buildDashboard()
|
|
4435
|
+
.finally(() => {
|
|
4436
|
+
usageDashboardPromise = null;
|
|
4437
|
+
});
|
|
4438
|
+
return await usageDashboardPromise;
|
|
2921
4439
|
},
|
|
2922
4440
|
getOnboardingStatus() {
|
|
2923
|
-
const nextConfig =
|
|
4441
|
+
const nextConfig = displayConfig();
|
|
2924
4442
|
return {
|
|
2925
4443
|
completed: nextConfig?.onboarding?.completed === true,
|
|
2926
4444
|
version: nextConfig?.onboarding?.version || 0,
|
|
@@ -2931,17 +4449,118 @@ function parsedProviderModelVersion(id) {
|
|
|
2931
4449
|
getAutoClear() {
|
|
2932
4450
|
return normalizeAutoClearConfig(config.autoClear);
|
|
2933
4451
|
},
|
|
4452
|
+
// --- User profile (/profile statusline command) ---------------------
|
|
4453
|
+
// getProfile returns the normalized { title, language } plus the resolved
|
|
4454
|
+
// language catalog entry and the full language list for the picker UI.
|
|
4455
|
+
getProfile() {
|
|
4456
|
+
const profile = cfgMod.normalizeProfileConfig(config.profile);
|
|
4457
|
+
return {
|
|
4458
|
+
...profile,
|
|
4459
|
+
languageEntry: cfgMod.profileLanguageEntry(profile.language),
|
|
4460
|
+
languages: cfgMod.PROFILE_LANGUAGES,
|
|
4461
|
+
};
|
|
4462
|
+
},
|
|
4463
|
+
// setProfile patches title and/or language and persists. Unknown language
|
|
4464
|
+
// ids normalize back to 'system'. Prompt-side injection is wired separately
|
|
4465
|
+
// (composeSystemPrompt) — this only owns the stored value.
|
|
4466
|
+
setProfile(input = {}) {
|
|
4467
|
+
const current = cfgMod.normalizeProfileConfig(config.profile);
|
|
4468
|
+
const next = { ...current };
|
|
4469
|
+
if (hasOwn(input, 'title') || hasOwn(input, 'name')) {
|
|
4470
|
+
next.title = input.title ?? input.name ?? '';
|
|
4471
|
+
}
|
|
4472
|
+
if (hasOwn(input, 'language') || hasOwn(input, 'lang')) {
|
|
4473
|
+
next.language = input.language ?? input.lang ?? 'system';
|
|
4474
|
+
}
|
|
4475
|
+
const normalized = cfgMod.normalizeProfileConfig(next);
|
|
4476
|
+
saveConfigAndAdopt({ ...config, profile: normalized });
|
|
4477
|
+
return this.getProfile();
|
|
4478
|
+
},
|
|
4479
|
+
getCompactionSettings() {
|
|
4480
|
+
return normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
|
|
4481
|
+
},
|
|
4482
|
+
setCompactionSettings(input = {}) {
|
|
4483
|
+
const current = normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
|
|
4484
|
+
const next = { ...current };
|
|
4485
|
+
if (hasOwn(input, 'auto')) next.auto = input.auto !== false;
|
|
4486
|
+
if (hasOwn(input, 'enabled')) next.auto = input.enabled !== false;
|
|
4487
|
+
if (hasOwn(input, 'type') || hasOwn(input, 'compactType') || hasOwn(input, 'compact_type')) {
|
|
4488
|
+
const requestedType = input.type ?? input.compactType ?? input.compact_type;
|
|
4489
|
+
const compactType = normalizeCompactTypeSetting(requestedType, current.compactType || current.type || 'semantic');
|
|
4490
|
+
if (compactType === 'recall-fasttrack' && !memoryEnabled()) {
|
|
4491
|
+
throw new Error('recall-fasttrack compact requires memory to be enabled');
|
|
4492
|
+
}
|
|
4493
|
+
next.type = compactType;
|
|
4494
|
+
next.compactType = compactType;
|
|
4495
|
+
}
|
|
4496
|
+
const nextConfig = { ...config };
|
|
4497
|
+
nextConfig.compaction = normalizeCompactionConfig(next, { memoryEnabled: memoryEnabled() });
|
|
4498
|
+
saveConfigAndAdopt(nextConfig);
|
|
4499
|
+
if (session) {
|
|
4500
|
+
session.compaction = {
|
|
4501
|
+
...(session.compaction || {}),
|
|
4502
|
+
...normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() }),
|
|
4503
|
+
};
|
|
4504
|
+
}
|
|
4505
|
+
invalidateContextStatusCache();
|
|
4506
|
+
return normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
|
|
4507
|
+
},
|
|
4508
|
+
getMemorySettings() {
|
|
4509
|
+
return {
|
|
4510
|
+
enabled: memoryEnabled(),
|
|
4511
|
+
compactFastTrackAvailable: memoryEnabled(),
|
|
4512
|
+
};
|
|
4513
|
+
},
|
|
4514
|
+
async setMemoryEnabled(enabled) {
|
|
4515
|
+
const nextConfig = setModuleEnabledInConfig({ ...config }, 'memory', enabled !== false);
|
|
4516
|
+
if (enabled === false) {
|
|
4517
|
+
nextConfig.compaction = normalizeCompactionConfig(nextConfig.compaction, { memoryEnabled: false });
|
|
4518
|
+
}
|
|
4519
|
+
saveConfigAndAdopt(nextConfig);
|
|
4520
|
+
if (!memoryEnabled() && memoryModPromise) {
|
|
4521
|
+
await memoryModPromise.then((mod) => mod?.stop?.()).catch(() => {});
|
|
4522
|
+
memoryModPromise = null;
|
|
4523
|
+
}
|
|
4524
|
+
if (session && config.compaction) {
|
|
4525
|
+
session.compaction = {
|
|
4526
|
+
...(session.compaction || {}),
|
|
4527
|
+
...normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() }),
|
|
4528
|
+
};
|
|
4529
|
+
}
|
|
4530
|
+
invalidatePreSessionToolSurface();
|
|
4531
|
+
invalidateContextStatusCache();
|
|
4532
|
+
return this.getMemorySettings();
|
|
4533
|
+
},
|
|
4534
|
+
getChannelSettings(options = {}) {
|
|
4535
|
+
return {
|
|
4536
|
+
enabled: channelsEnabled(),
|
|
4537
|
+
...(options?.includeStatus === false ? {} : { status: channels.status() }),
|
|
4538
|
+
};
|
|
4539
|
+
},
|
|
4540
|
+
async setChannelsEnabled(enabled) {
|
|
4541
|
+
const nextConfig = setModuleEnabledInConfig({ ...config }, 'channels', enabled !== false);
|
|
4542
|
+
saveConfigAndAdopt(nextConfig);
|
|
4543
|
+
if (!channelsEnabled()) {
|
|
4544
|
+
if (channelStartTimer) {
|
|
4545
|
+
clearTimeout(channelStartTimer);
|
|
4546
|
+
channelStartTimer = null;
|
|
4547
|
+
}
|
|
4548
|
+
await channels.stop('settings-disabled', { waitForExit: false }).catch(() => {});
|
|
4549
|
+
} else {
|
|
4550
|
+
scheduleChannelStart(0);
|
|
4551
|
+
}
|
|
4552
|
+
invalidatePreSessionToolSurface();
|
|
4553
|
+
return this.getChannelSettings();
|
|
4554
|
+
},
|
|
2934
4555
|
getSystemShell() {
|
|
2935
4556
|
return normalizeSystemShellConfig(config.shell);
|
|
2936
4557
|
},
|
|
2937
4558
|
setSystemShell(input = {}) {
|
|
2938
4559
|
const command = normalizeSystemShellCommand(typeof input === 'string' ? input : input?.command);
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
...
|
|
2942
|
-
shell: command ? { ...(nextConfig.shell || {}), command } : {},
|
|
4560
|
+
saveConfigAndAdopt({
|
|
4561
|
+
...config,
|
|
4562
|
+
shell: command ? { ...(config.shell || {}), command } : {},
|
|
2943
4563
|
});
|
|
2944
|
-
config = cfgMod.loadConfig();
|
|
2945
4564
|
setConfiguredShell(command);
|
|
2946
4565
|
return normalizeSystemShellConfig(config.shell);
|
|
2947
4566
|
},
|
|
@@ -2960,9 +4579,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2960
4579
|
next.idleMs = idleMs;
|
|
2961
4580
|
if (!hasOwn(input, 'enabled')) next.enabled = true;
|
|
2962
4581
|
}
|
|
2963
|
-
|
|
2964
|
-
cfgMod.saveConfig({ ...nextConfig, autoClear: next });
|
|
2965
|
-
config = cfgMod.loadConfig();
|
|
4582
|
+
saveConfigAndAdopt({ ...config, autoClear: next });
|
|
2966
4583
|
return { ...normalizeAutoClearConfig(config.autoClear), label: formatDurationMs(normalizeAutoClearConfig(config.autoClear).idleMs) };
|
|
2967
4584
|
},
|
|
2968
4585
|
async completeOnboarding(payload = {}) {
|
|
@@ -2970,7 +4587,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2970
4587
|
const workflowInput = payload.workflowRoutes && typeof payload.workflowRoutes === 'object'
|
|
2971
4588
|
? payload.workflowRoutes
|
|
2972
4589
|
: {};
|
|
2973
|
-
const nextConfig =
|
|
4590
|
+
const nextConfig = { ...config };
|
|
2974
4591
|
let presets = Array.isArray(nextConfig.presets) ? nextConfig.presets.slice() : [];
|
|
2975
4592
|
const workflowRoutes = { ...(nextConfig.workflowRoutes || {}) };
|
|
2976
4593
|
|
|
@@ -2989,10 +4606,13 @@ function parsedProviderModelVersion(id) {
|
|
|
2989
4606
|
|
|
2990
4607
|
nextConfig.presets = presets;
|
|
2991
4608
|
nextConfig.workflowRoutes = workflowRoutes;
|
|
4609
|
+
// Maintenance slots store a direct {provider, model} route. Reuse the
|
|
4610
|
+
// onboarding workflow routes directly; only overwrite a slot when its
|
|
4611
|
+
// workflow route was actually provided, otherwise keep the existing slot.
|
|
2992
4612
|
nextConfig.maintenance = {
|
|
2993
4613
|
...(nextConfig.maintenance || {}),
|
|
2994
|
-
|
|
2995
|
-
|
|
4614
|
+
...(workflowRoutes.explorer ? { explore: normalizeWorkflowRoute(workflowRoutes.explorer) } : {}),
|
|
4615
|
+
...(workflowRoutes.memory ? { memory: normalizeWorkflowRoute(workflowRoutes.memory) } : {}),
|
|
2996
4616
|
};
|
|
2997
4617
|
nextConfig.onboarding = {
|
|
2998
4618
|
...(nextConfig.onboarding || {}),
|
|
@@ -3001,8 +4621,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3001
4621
|
completedAt: new Date().toISOString(),
|
|
3002
4622
|
};
|
|
3003
4623
|
|
|
3004
|
-
|
|
3005
|
-
config = cfgMod.loadConfig();
|
|
4624
|
+
saveConfigAndAdopt(nextConfig);
|
|
3006
4625
|
if (defaultRoute) {
|
|
3007
4626
|
route = resolveRoute(config, { provider: defaultRoute.provider, model: defaultRoute.model, effort: defaultRoute.effort });
|
|
3008
4627
|
if (session?.id) mgr.closeSession(session.id, 'cli-onboarding-complete');
|
|
@@ -3085,26 +4704,34 @@ function parsedProviderModelVersion(id) {
|
|
|
3085
4704
|
const result = String(secret || '').trim()
|
|
3086
4705
|
? saveProviderApiKey(cfgMod, providerId, secret)
|
|
3087
4706
|
: await loginOAuthProvider(cfgMod, providerId);
|
|
3088
|
-
|
|
4707
|
+
reloadFullConfig();
|
|
3089
4708
|
invalidateProviderCaches();
|
|
3090
4709
|
warmProviderModelCache();
|
|
3091
4710
|
return result;
|
|
3092
4711
|
},
|
|
3093
4712
|
async loginOAuthProvider(providerId) {
|
|
3094
4713
|
const result = await loginOAuthProvider(cfgMod, providerId);
|
|
3095
|
-
|
|
4714
|
+
reloadFullConfig();
|
|
3096
4715
|
invalidateProviderCaches();
|
|
3097
4716
|
warmProviderModelCache();
|
|
3098
4717
|
return result;
|
|
3099
4718
|
},
|
|
3100
4719
|
async beginOAuthProviderLogin(providerId) {
|
|
3101
4720
|
const result = await beginOAuthProviderLogin(cfgMod, providerId);
|
|
3102
|
-
|
|
4721
|
+
reloadFullConfig();
|
|
3103
4722
|
return {
|
|
3104
4723
|
...result,
|
|
4724
|
+
waitForCallback: result.waitForCallback?.then((completed) => {
|
|
4725
|
+
reloadFullConfig();
|
|
4726
|
+
if (completed) {
|
|
4727
|
+
invalidateProviderCaches();
|
|
4728
|
+
warmProviderModelCache();
|
|
4729
|
+
}
|
|
4730
|
+
return completed;
|
|
4731
|
+
}),
|
|
3105
4732
|
completeCode: async (code) => {
|
|
3106
4733
|
const completed = await result.completeCode(code);
|
|
3107
|
-
|
|
4734
|
+
reloadFullConfig();
|
|
3108
4735
|
invalidateProviderCaches();
|
|
3109
4736
|
warmProviderModelCache();
|
|
3110
4737
|
return completed;
|
|
@@ -3113,42 +4740,97 @@ function parsedProviderModelVersion(id) {
|
|
|
3113
4740
|
},
|
|
3114
4741
|
saveProviderApiKey(providerId, secret) {
|
|
3115
4742
|
const result = saveProviderApiKey(cfgMod, providerId, secret);
|
|
3116
|
-
|
|
4743
|
+
reloadFullConfig();
|
|
3117
4744
|
invalidateProviderCaches();
|
|
3118
4745
|
warmProviderModelCache();
|
|
3119
4746
|
return result;
|
|
3120
4747
|
},
|
|
3121
4748
|
saveOpenAIUsageSessionKey(secret) {
|
|
3122
4749
|
const result = saveOpenAIUsageSessionKey(cfgMod, secret);
|
|
3123
|
-
|
|
4750
|
+
reloadFullConfig();
|
|
3124
4751
|
invalidateProviderCaches();
|
|
3125
4752
|
return result;
|
|
3126
4753
|
},
|
|
3127
4754
|
saveOpenCodeGoUsageAuth(opts) {
|
|
3128
4755
|
const result = saveOpenCodeGoUsageAuth(cfgMod, opts);
|
|
3129
|
-
|
|
4756
|
+
reloadFullConfig();
|
|
3130
4757
|
invalidateProviderCaches();
|
|
3131
4758
|
return result;
|
|
3132
4759
|
},
|
|
3133
4760
|
setLocalProvider(providerId, opts) {
|
|
3134
4761
|
const result = setLocalProvider(cfgMod, providerId, opts);
|
|
3135
|
-
|
|
4762
|
+
reloadFullConfig();
|
|
3136
4763
|
invalidateProviderCaches();
|
|
3137
4764
|
warmProviderModelCache();
|
|
3138
4765
|
return result;
|
|
3139
4766
|
},
|
|
3140
4767
|
forgetProviderAuth(providerId) {
|
|
3141
|
-
const result = forgetProviderAuth(providerId);
|
|
3142
|
-
|
|
4768
|
+
const result = forgetProviderAuth(cfgMod, providerId);
|
|
4769
|
+
reloadFullConfig();
|
|
3143
4770
|
invalidateProviderCaches();
|
|
3144
4771
|
warmProviderModelCache();
|
|
3145
4772
|
return result;
|
|
3146
4773
|
},
|
|
3147
4774
|
listPresets() {
|
|
3148
|
-
return cfgMod.listPresets(
|
|
4775
|
+
return cfgMod.listPresets(displayConfig());
|
|
3149
4776
|
},
|
|
3150
4777
|
async listProviderModels(options = {}) {
|
|
3151
|
-
return await collectProviderModels({
|
|
4778
|
+
return await collectProviderModels({
|
|
4779
|
+
force: options.force === true || options.refresh === true,
|
|
4780
|
+
quick: options.quick === true,
|
|
4781
|
+
});
|
|
4782
|
+
},
|
|
4783
|
+
getSearchRoute() {
|
|
4784
|
+
searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
4785
|
+
return searchRoute;
|
|
4786
|
+
},
|
|
4787
|
+
async listSearchModels(options = {}) {
|
|
4788
|
+
return await collectSearchProviderModels({ force: options.force === true || options.refresh === true });
|
|
4789
|
+
},
|
|
4790
|
+
async setSearchRoute(next) {
|
|
4791
|
+
let selectedRoute = normalizeSearchRouteConfig(next);
|
|
4792
|
+
if (!selectedRoute && next?.model && searchRoute?.provider) {
|
|
4793
|
+
selectedRoute = normalizeSearchRouteConfig({ ...next, provider: searchRoute.provider });
|
|
4794
|
+
}
|
|
4795
|
+
if (!selectedRoute) throw new Error('search route requires provider and model');
|
|
4796
|
+
if (isDefaultSearchRouteConfig(selectedRoute)) {
|
|
4797
|
+
ensureFullConfig();
|
|
4798
|
+
const routeToSave = normalizeSearchRouteConfig({
|
|
4799
|
+
provider: SEARCH_DEFAULT_PROVIDER,
|
|
4800
|
+
model: SEARCH_DEFAULT_MODEL,
|
|
4801
|
+
...(selectedRoute.toolType ? { toolType: selectedRoute.toolType } : {}),
|
|
4802
|
+
});
|
|
4803
|
+
const nextConfig = { ...config };
|
|
4804
|
+
nextConfig.searchRoute = routeToSave;
|
|
4805
|
+
saveConfigAndAdopt(nextConfig);
|
|
4806
|
+
searchRoute = normalizeSearchRouteConfig(config.searchRoute);
|
|
4807
|
+
invalidateProviderCaches();
|
|
4808
|
+
return searchRoute;
|
|
4809
|
+
}
|
|
4810
|
+
if (!isSearchCapableProvider(selectedRoute.provider)) {
|
|
4811
|
+
throw new Error(`provider "${selectedRoute.provider}" does not support Mixdog native search`);
|
|
4812
|
+
}
|
|
4813
|
+
ensureFullConfig();
|
|
4814
|
+
await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
|
|
4815
|
+
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
4816
|
+
if (!searchCapableFor(selectedRoute.provider, modelMeta)) {
|
|
4817
|
+
throw new Error(`model "${selectedRoute.model}" is not marked as web-search capable`);
|
|
4818
|
+
}
|
|
4819
|
+
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
4820
|
+
const effort = coerceEffortFor(selectedRoute.provider, modelMeta, selectedRoute.effort);
|
|
4821
|
+
selectedRoute = {
|
|
4822
|
+
...selectedRoute,
|
|
4823
|
+
...(effort ? { effort } : {}),
|
|
4824
|
+
fast: fastCapable ? selectedRoute.fast === true : false,
|
|
4825
|
+
};
|
|
4826
|
+
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
4827
|
+
const routeToSave = normalizeSearchRouteConfig(selectedRoute);
|
|
4828
|
+
const nextConfig = { ...config };
|
|
4829
|
+
nextConfig.searchRoute = routeToSave;
|
|
4830
|
+
saveConfigAndAdopt(nextConfig);
|
|
4831
|
+
searchRoute = normalizeSearchRouteConfig(config.searchRoute);
|
|
4832
|
+
invalidateProviderCaches();
|
|
4833
|
+
return searchRoute;
|
|
3152
4834
|
},
|
|
3153
4835
|
listAgents() {
|
|
3154
4836
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
@@ -3160,7 +4842,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3160
4842
|
}));
|
|
3161
4843
|
},
|
|
3162
4844
|
listWorkflows() {
|
|
3163
|
-
const currentConfig =
|
|
4845
|
+
const currentConfig = displayConfig();
|
|
3164
4846
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
3165
4847
|
const active = activeWorkflowId(currentConfig);
|
|
3166
4848
|
return listWorkflowPacks(dataDir).map((workflow) => ({
|
|
@@ -3173,16 +4855,13 @@ function parsedProviderModelVersion(id) {
|
|
|
3173
4855
|
}));
|
|
3174
4856
|
},
|
|
3175
4857
|
getOutputStyle() {
|
|
3176
|
-
|
|
3177
|
-
return outputStyleStatus(dataDir);
|
|
4858
|
+
return getOutputStyleStatusCached();
|
|
3178
4859
|
},
|
|
3179
4860
|
listOutputStyles() {
|
|
3180
|
-
|
|
3181
|
-
return outputStyleStatus(dataDir);
|
|
4861
|
+
return getOutputStyleStatusCached();
|
|
3182
4862
|
},
|
|
3183
4863
|
async setOutputStyle(value) {
|
|
3184
|
-
const
|
|
3185
|
-
const before = outputStyleStatus(dataDir);
|
|
4864
|
+
const before = getOutputStyleStatusCached({ fresh: true });
|
|
3186
4865
|
const selected = findOutputStyle(value, before.styles);
|
|
3187
4866
|
if (!selected) {
|
|
3188
4867
|
const names = before.styles.map((style) => style.label || style.id).join(', ') || 'Default';
|
|
@@ -3198,6 +4877,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3198
4877
|
}
|
|
3199
4878
|
return next;
|
|
3200
4879
|
});
|
|
4880
|
+
invalidateOutputStyleStatusCache();
|
|
3201
4881
|
const hasConversation = sessionHasConversationMessages(session);
|
|
3202
4882
|
let appliedToCurrentSession = !hasConversation;
|
|
3203
4883
|
if (session?.id && !hasConversation) {
|
|
@@ -3206,23 +4886,17 @@ function parsedProviderModelVersion(id) {
|
|
|
3206
4886
|
await recreateCurrentSessionIfReady();
|
|
3207
4887
|
}
|
|
3208
4888
|
invalidateContextStatusCache();
|
|
3209
|
-
return { ...
|
|
4889
|
+
return { ...getOutputStyleStatusCached({ fresh: true }), appliedToCurrentSession };
|
|
3210
4890
|
},
|
|
3211
4891
|
async setWorkflow(workflowId) {
|
|
3212
4892
|
const id = normalizeWorkflowId(workflowId, DEFAULT_WORKFLOW_ID);
|
|
3213
4893
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
3214
4894
|
const pack = loadWorkflowPack(dataDir, id);
|
|
3215
4895
|
if (!pack || pack.id !== id) throw new Error(`workflow "${workflowId}" not found`);
|
|
3216
|
-
const nextConfig =
|
|
4896
|
+
const nextConfig = { ...config };
|
|
3217
4897
|
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 };
|
|
4898
|
+
saveConfigAndAdopt(nextConfig);
|
|
4899
|
+
return workflowSummary(pack);
|
|
3226
4900
|
},
|
|
3227
4901
|
async setAgentRoute(agentId, next) {
|
|
3228
4902
|
const id = normalizeAgentId(agentId);
|
|
@@ -3232,12 +4906,12 @@ function parsedProviderModelVersion(id) {
|
|
|
3232
4906
|
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
3233
4907
|
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
3234
4908
|
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
3235
|
-
|
|
4909
|
+
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
3236
4910
|
|
|
3237
4911
|
const routeToSave = normalizeWorkflowRoute(selectedRoute);
|
|
3238
4912
|
if (!routeToSave) throw new Error('agent route requires provider and model');
|
|
3239
4913
|
const agent = FIXED_AGENT_SLOTS.find((item) => item.id === id);
|
|
3240
|
-
const nextConfig =
|
|
4914
|
+
const nextConfig = { ...config };
|
|
3241
4915
|
nextConfig.agents = {
|
|
3242
4916
|
...(nextConfig.agents || {}),
|
|
3243
4917
|
[id]: routeToSave,
|
|
@@ -3249,24 +4923,48 @@ function parsedProviderModelVersion(id) {
|
|
|
3249
4923
|
[agent.workflowSlot]: routeToSave,
|
|
3250
4924
|
};
|
|
3251
4925
|
nextConfig.presets = upsertWorkflowPreset(nextConfig.presets, agent.workflowSlot, routeToSave);
|
|
4926
|
+
// Maintenance slots store a direct {provider, model} route now, so
|
|
4927
|
+
// mirror the agent route straight in instead of a preset-id string.
|
|
3252
4928
|
nextConfig.maintenance = {
|
|
3253
4929
|
...(nextConfig.maintenance || {}),
|
|
3254
|
-
...(id === 'explore' ? { explore:
|
|
3255
|
-
...(id === 'maintainer' ? { memory:
|
|
4930
|
+
...(id === 'explore' ? { explore: routeToSave } : {}),
|
|
4931
|
+
...(id === 'maintainer' ? { memory: routeToSave } : {}),
|
|
3256
4932
|
};
|
|
3257
4933
|
}
|
|
3258
|
-
|
|
3259
|
-
config = cfgMod.loadConfig();
|
|
4934
|
+
saveConfigAndAdopt(nextConfig);
|
|
3260
4935
|
return routeToSave;
|
|
3261
4936
|
},
|
|
3262
4937
|
async ask(prompt, options = {}) {
|
|
3263
4938
|
activeTurnCount += 1;
|
|
4939
|
+
// Lazy code-graph prewarm: kick off the build ONCE, on the first real
|
|
4940
|
+
// turn, so a likely code lookup hits a warm cache — without paying the
|
|
4941
|
+
// post-first-frame prewarm freeze on idle startup. The schedule is async
|
|
4942
|
+
// + unref'd (worker thread), so it never blocks this turn.
|
|
4943
|
+
if (codeGraphPrewarmLazy && !codeGraphFirstTurnPrewarmDone) {
|
|
4944
|
+
codeGraphFirstTurnPrewarmDone = true;
|
|
4945
|
+
scheduleCodeGraphPrewarm(0, 'first-turn');
|
|
4946
|
+
}
|
|
3264
4947
|
const startedAt = Date.now();
|
|
3265
4948
|
try {
|
|
3266
4949
|
await refreshSessionForCwdIfNeeded('cwd-change');
|
|
3267
4950
|
if (!session?.id) await createCurrentSession('turn');
|
|
3268
4951
|
hooks.emit('turn:start', { sessionId: session.id, prompt, cwd: currentCwd });
|
|
3269
|
-
|
|
4952
|
+
// UserPromptSubmit: bridge to the standard hook bus. A hook FAILURE
|
|
4953
|
+
// must not block the turn, but a genuine blocked===true MUST throw.
|
|
4954
|
+
let promptDispatch = null;
|
|
4955
|
+
try {
|
|
4956
|
+
promptDispatch = await hooks.dispatch('UserPromptSubmit', hookCommonPayload({ session_id: session.id, prompt }));
|
|
4957
|
+
} catch { /* hook failure never blocks the turn */ }
|
|
4958
|
+
if (promptDispatch?.blocked === true) {
|
|
4959
|
+
throw new Error(`prompt blocked by hook: ${promptDispatch.reason || ''}`);
|
|
4960
|
+
}
|
|
4961
|
+
const hookContext = Array.isArray(promptDispatch?.additionalContext)
|
|
4962
|
+
? promptDispatch.additionalContext.join('\n\n')
|
|
4963
|
+
: String(promptDispatch?.additionalContext || '');
|
|
4964
|
+
const turnContext = [options.context || '', hookContext]
|
|
4965
|
+
.map((part) => String(part || '').trim())
|
|
4966
|
+
.filter(Boolean)
|
|
4967
|
+
.join('\n\n');
|
|
3270
4968
|
const result = await mgr.askSession(
|
|
3271
4969
|
session.id,
|
|
3272
4970
|
prompt,
|
|
@@ -3289,16 +4987,25 @@ function parsedProviderModelVersion(id) {
|
|
|
3289
4987
|
{
|
|
3290
4988
|
onTextDelta: options.onTextDelta,
|
|
3291
4989
|
onReasoningDelta: options.onReasoningDelta,
|
|
4990
|
+
onAssistantText: options.onAssistantText,
|
|
3292
4991
|
onUsageDelta: options.onUsageDelta,
|
|
3293
4992
|
onToolResult: options.onToolResult,
|
|
4993
|
+
onToolApproval: options.onToolApproval,
|
|
4994
|
+
onCompactEvent: options.onCompactEvent,
|
|
3294
4995
|
onStageChange: options.onStageChange,
|
|
3295
4996
|
onStreamDelta: options.onStreamDelta,
|
|
3296
4997
|
drainSteering: options.drainSteering,
|
|
3297
4998
|
onSteerMessage: options.onSteerMessage,
|
|
4999
|
+
notifyFn: notifyFnForSession(session.id),
|
|
3298
5000
|
},
|
|
3299
5001
|
);
|
|
3300
5002
|
session = mgr.getSession(session.id) || session;
|
|
3301
5003
|
hooks.emit('turn:end', { sessionId: session.id, elapsedMs: Date.now() - startedAt });
|
|
5004
|
+
// Stop: bridge to the standard hook bus. Best-effort; ignore result,
|
|
5005
|
+
// never throw.
|
|
5006
|
+
try {
|
|
5007
|
+
await hooks.dispatch('Stop', hookCommonPayload({ session_id: session.id }));
|
|
5008
|
+
} catch { /* best-effort: Stop hook must never break the turn */ }
|
|
3302
5009
|
return { result, session };
|
|
3303
5010
|
} catch (error) {
|
|
3304
5011
|
hooks.emit('turn:error', { sessionId: session?.id || null, elapsedMs: Date.now() - startedAt, error: error?.message || String(error) });
|
|
@@ -3307,27 +5014,31 @@ function parsedProviderModelVersion(id) {
|
|
|
3307
5014
|
activeTurnCount = Math.max(0, activeTurnCount - 1);
|
|
3308
5015
|
if (!firstTurnCompleted) {
|
|
3309
5016
|
firstTurnCompleted = true;
|
|
5017
|
+
scheduleProviderWarmup();
|
|
3310
5018
|
scheduleProviderModelWarmup();
|
|
3311
5019
|
}
|
|
3312
5020
|
}
|
|
3313
5021
|
},
|
|
3314
5022
|
async clear(options = {}) {
|
|
3315
5023
|
if (!session?.id) return false;
|
|
3316
|
-
const cleared = await mgr.clearSessionMessages(session.id);
|
|
5024
|
+
const cleared = await mgr.clearSessionMessages(session.id, options);
|
|
3317
5025
|
if (!cleared) return false;
|
|
3318
5026
|
session = typeof cleared === 'object' ? cleared : (mgr.getSession(session.id) || session);
|
|
3319
|
-
if (options.
|
|
3320
|
-
try {
|
|
5027
|
+
if (options.recoverAgent === true) {
|
|
5028
|
+
try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
|
|
3321
5029
|
}
|
|
3322
5030
|
invalidateContextStatusCache();
|
|
3323
5031
|
return true;
|
|
3324
5032
|
},
|
|
3325
5033
|
async compact(options = {}) {
|
|
3326
5034
|
if (!session?.id) return null;
|
|
5035
|
+
if (activeTurnCount > 0) {
|
|
5036
|
+
return { changed: false, reason: 'compact skipped: turn in progress' };
|
|
5037
|
+
}
|
|
3327
5038
|
const result = await mgr.compactSessionMessages(session.id);
|
|
3328
5039
|
session = mgr.getSession(session.id) || session;
|
|
3329
|
-
if (options.
|
|
3330
|
-
try {
|
|
5040
|
+
if (options.recoverAgent === true) {
|
|
5041
|
+
try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
|
|
3331
5042
|
}
|
|
3332
5043
|
invalidateContextStatusCache();
|
|
3333
5044
|
return result;
|
|
@@ -3339,25 +5050,15 @@ function parsedProviderModelVersion(id) {
|
|
|
3339
5050
|
await recreateCurrentSessionIfReady();
|
|
3340
5051
|
return mode;
|
|
3341
5052
|
},
|
|
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();
|
|
5053
|
+
agentStatus() {
|
|
5054
|
+
return agentStatusState();
|
|
3354
5055
|
},
|
|
3355
5056
|
get clientHostPid() {
|
|
3356
5057
|
return session?.clientHostPid || process.pid;
|
|
3357
5058
|
},
|
|
3358
|
-
|
|
5059
|
+
agentControl(args = {}) {
|
|
3359
5060
|
const callerSessionId = session?.id || null;
|
|
3360
|
-
return
|
|
5061
|
+
return agentTool.execute(args, {
|
|
3361
5062
|
callerCwd: currentCwd,
|
|
3362
5063
|
invocationSource: 'user-command',
|
|
3363
5064
|
callerSessionId,
|
|
@@ -3387,6 +5088,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3387
5088
|
activeCount: rows.filter((row) => row.active).length,
|
|
3388
5089
|
tools,
|
|
3389
5090
|
activeTools: sortedNamesByMeasuredUsage(activeNames),
|
|
5091
|
+
discoveredTools: sortedNamesByMeasuredUsage(surface?.deferredDiscoveredTools || []),
|
|
3390
5092
|
};
|
|
3391
5093
|
},
|
|
3392
5094
|
selectTools(names) {
|
|
@@ -3402,7 +5104,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3402
5104
|
return mcpStatus();
|
|
3403
5105
|
},
|
|
3404
5106
|
async reconnectMcp() {
|
|
3405
|
-
|
|
5107
|
+
reloadFullConfig();
|
|
3406
5108
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3407
5109
|
invalidatePreSessionToolSurface();
|
|
3408
5110
|
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-reconnect');
|
|
@@ -3411,13 +5113,12 @@ function parsedProviderModelVersion(id) {
|
|
|
3411
5113
|
},
|
|
3412
5114
|
async addMcpServer(input = {}) {
|
|
3413
5115
|
const { name, config: serverConfig } = normalizeMcpServerInput(input);
|
|
3414
|
-
const nextConfig =
|
|
5116
|
+
const nextConfig = { ...config };
|
|
3415
5117
|
nextConfig.mcpServers = {
|
|
3416
5118
|
...(nextConfig.mcpServers || {}),
|
|
3417
5119
|
[name]: serverConfig,
|
|
3418
5120
|
};
|
|
3419
|
-
|
|
3420
|
-
config = cfgMod.loadConfig();
|
|
5121
|
+
saveConfigAndAdopt(nextConfig);
|
|
3421
5122
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3422
5123
|
invalidatePreSessionToolSurface();
|
|
3423
5124
|
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-add');
|
|
@@ -3427,7 +5128,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3427
5128
|
async removeMcpServer(name) {
|
|
3428
5129
|
const serverName = clean(name);
|
|
3429
5130
|
if (!serverName) throw new Error('MCP server name is required');
|
|
3430
|
-
const nextConfig =
|
|
5131
|
+
const nextConfig = { ...config };
|
|
3431
5132
|
const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
|
|
3432
5133
|
? { ...nextConfig.mcpServers }
|
|
3433
5134
|
: {};
|
|
@@ -3435,8 +5136,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3435
5136
|
throw new Error(`MCP server not configured: ${serverName}`);
|
|
3436
5137
|
}
|
|
3437
5138
|
delete current[serverName];
|
|
3438
|
-
|
|
3439
|
-
config = cfgMod.loadConfig();
|
|
5139
|
+
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
3440
5140
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3441
5141
|
invalidatePreSessionToolSurface();
|
|
3442
5142
|
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-remove');
|
|
@@ -3446,7 +5146,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3446
5146
|
async setMcpServerEnabled(name, enabled) {
|
|
3447
5147
|
const serverName = clean(name);
|
|
3448
5148
|
if (!serverName) throw new Error('MCP server name is required');
|
|
3449
|
-
const nextConfig =
|
|
5149
|
+
const nextConfig = { ...config };
|
|
3450
5150
|
const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
|
|
3451
5151
|
? { ...nextConfig.mcpServers }
|
|
3452
5152
|
: {};
|
|
@@ -3454,8 +5154,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3454
5154
|
throw new Error(`MCP server not configured: ${serverName}`);
|
|
3455
5155
|
}
|
|
3456
5156
|
current[serverName] = { ...(current[serverName] || {}), enabled: enabled !== false };
|
|
3457
|
-
|
|
3458
|
-
config = cfgMod.loadConfig();
|
|
5157
|
+
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
3459
5158
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3460
5159
|
invalidatePreSessionToolSurface();
|
|
3461
5160
|
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-toggle');
|
|
@@ -3506,13 +5205,12 @@ function parsedProviderModelVersion(id) {
|
|
|
3506
5205
|
const key = clean(plugin.id || plugin.name || plugin);
|
|
3507
5206
|
const dataDir = cfgMod.getPluginData?.();
|
|
3508
5207
|
const removed = registryRemovePlugin(key, { dataDir });
|
|
3509
|
-
const nextConfig =
|
|
5208
|
+
const nextConfig = { ...config };
|
|
3510
5209
|
const serverName = pluginMcpServerName(plugin);
|
|
3511
5210
|
if (nextConfig.mcpServers && Object.prototype.hasOwnProperty.call(nextConfig.mcpServers, serverName)) {
|
|
3512
5211
|
const current = { ...nextConfig.mcpServers };
|
|
3513
5212
|
delete current[serverName];
|
|
3514
|
-
|
|
3515
|
-
config = cfgMod.loadConfig();
|
|
5213
|
+
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
3516
5214
|
await connectConfiguredMcp({ reset: true });
|
|
3517
5215
|
invalidatePreSessionToolSurface();
|
|
3518
5216
|
}
|
|
@@ -3527,7 +5225,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3527
5225
|
const scriptPath = join(root, script);
|
|
3528
5226
|
if (!existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${scriptPath}`);
|
|
3529
5227
|
const serverName = pluginMcpServerName(plugin);
|
|
3530
|
-
const nextConfig =
|
|
5228
|
+
const nextConfig = { ...config };
|
|
3531
5229
|
nextConfig.mcpServers = {
|
|
3532
5230
|
...(nextConfig.mcpServers || {}),
|
|
3533
5231
|
[serverName]: {
|
|
@@ -3540,8 +5238,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3540
5238
|
},
|
|
3541
5239
|
},
|
|
3542
5240
|
};
|
|
3543
|
-
|
|
3544
|
-
config = cfgMod.loadConfig();
|
|
5241
|
+
saveConfigAndAdopt(nextConfig);
|
|
3545
5242
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3546
5243
|
invalidatePreSessionToolSurface();
|
|
3547
5244
|
if (session?.id) mgr.closeSession(session.id, 'cli-plugin-mcp-enable');
|
|
@@ -3609,8 +5306,11 @@ function parsedProviderModelVersion(id) {
|
|
|
3609
5306
|
}
|
|
3610
5307
|
return result;
|
|
3611
5308
|
},
|
|
3612
|
-
async setRoute(next) {
|
|
5309
|
+
async setRoute(next, options = {}) {
|
|
5310
|
+
const applyToCurrentSession = options?.applyToCurrentSession !== false;
|
|
3613
5311
|
const requested = { ...(next || {}) };
|
|
5312
|
+
validateRequestedModelSelector(config, requested);
|
|
5313
|
+
const providerExplicitlyRequested = clean(next?.provider) !== '';
|
|
3614
5314
|
if (requested.effort === undefined && !requested.provider && !requested.model && hasOwn(route, 'effort')) {
|
|
3615
5315
|
requested.effort = route.effort;
|
|
3616
5316
|
}
|
|
@@ -3620,14 +5320,34 @@ function parsedProviderModelVersion(id) {
|
|
|
3620
5320
|
let selectedRoute = resolveRoute(config, requested);
|
|
3621
5321
|
await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
|
|
3622
5322
|
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
5323
|
+
// Guard (A안 / strict reject): a free-text string must never become the
|
|
5324
|
+
// lead model. When the caller did not pin a provider, the route did not
|
|
5325
|
+
// match a preset, and the model is unknown to both the live provider
|
|
5326
|
+
// catalog (modelMeta is still the {id,provider} placeholder) and the
|
|
5327
|
+
// offline catalog, refuse BEFORE any config write so a partial save
|
|
5328
|
+
// (saveModelSettings/adoptConfig/persistLeadRoute) can never land.
|
|
5329
|
+
if (!providerExplicitlyRequested
|
|
5330
|
+
&& !selectedRoute.preset
|
|
5331
|
+
&& !modelMetaLooksResolved(modelMeta)
|
|
5332
|
+
&& !getModelMetadataSync(selectedRoute.model, selectedRoute.provider)) {
|
|
5333
|
+
throw new Error(`unknown model: ${selectedRoute.provider}/${selectedRoute.model}`);
|
|
5334
|
+
}
|
|
3623
5335
|
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
3624
5336
|
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
3625
|
-
|
|
5337
|
+
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
5338
|
+
if (!applyToCurrentSession) {
|
|
5339
|
+
persistLeadRoute(selectedRoute);
|
|
5340
|
+
refreshStatuslineUsageSnapshot(route);
|
|
5341
|
+
scheduleStatuslineUsageRefresh();
|
|
5342
|
+
return selectedRoute;
|
|
5343
|
+
}
|
|
3626
5344
|
const leadRoute = persistLeadRoute(selectedRoute);
|
|
3627
5345
|
route = resolveRoute(config, leadRoute
|
|
3628
5346
|
? { model: workflowPresetId('lead') }
|
|
3629
5347
|
: selectedRoute);
|
|
3630
5348
|
await refreshRouteEffort(modelMeta);
|
|
5349
|
+
refreshStatuslineUsageSnapshot(route);
|
|
5350
|
+
scheduleStatuslineUsageRefresh();
|
|
3631
5351
|
if (session) {
|
|
3632
5352
|
const updated = mgr.updateSessionRoute?.(session.id, {
|
|
3633
5353
|
provider: route.provider,
|
|
@@ -3642,7 +5362,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3642
5362
|
session.fast = route.fast === true;
|
|
3643
5363
|
session.effort = route.effectiveEffort || null;
|
|
3644
5364
|
}
|
|
3645
|
-
statusRoutes
|
|
5365
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
3646
5366
|
invalidateContextStatusCache();
|
|
3647
5367
|
}
|
|
3648
5368
|
return route;
|
|
@@ -3656,14 +5376,14 @@ function parsedProviderModelVersion(id) {
|
|
|
3656
5376
|
throw new Error(`fast mode is not available for ${route.provider}/${route.model}`);
|
|
3657
5377
|
}
|
|
3658
5378
|
route = resolveRoute(config, { provider: route.provider, model: route.model, effort: route.effort, fast: fastCapable ? enabled : false });
|
|
3659
|
-
|
|
5379
|
+
adoptConfig(saveModelSettings(cfgMod, route, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
3660
5380
|
const leadRoute = persistLeadRoute(route);
|
|
3661
5381
|
if (leadRoute) route = resolveRoute(config, { model: workflowPresetId('lead') });
|
|
3662
5382
|
await refreshRouteEffort(modelMeta);
|
|
3663
5383
|
if (session) {
|
|
3664
5384
|
session.fast = route.fast === true;
|
|
3665
5385
|
session.effort = route.effectiveEffort || null;
|
|
3666
|
-
statusRoutes
|
|
5386
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
3667
5387
|
invalidateContextStatusCache();
|
|
3668
5388
|
}
|
|
3669
5389
|
return route.fast === true;
|
|
@@ -3676,15 +5396,17 @@ function parsedProviderModelVersion(id) {
|
|
|
3676
5396
|
async setEffort(value) {
|
|
3677
5397
|
const normalized = normalizeEffortInput(value);
|
|
3678
5398
|
route = { ...route, effort: normalized };
|
|
3679
|
-
|
|
5399
|
+
const modelMeta = await lookupModelMeta(route.provider, route.model);
|
|
5400
|
+
const fastCapable = fastCapableFor(route.provider, modelMeta);
|
|
5401
|
+
adoptConfig(saveModelSettings(cfgMod, route, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
3680
5402
|
const leadRoute = persistLeadRoute(route);
|
|
3681
5403
|
if (leadRoute) {
|
|
3682
5404
|
route = resolveRoute(config, { model: workflowPresetId('lead') });
|
|
3683
5405
|
}
|
|
3684
|
-
await refreshRouteEffort();
|
|
5406
|
+
await refreshRouteEffort(modelMeta);
|
|
3685
5407
|
if (session) {
|
|
3686
5408
|
session.effort = route.effectiveEffort || null;
|
|
3687
|
-
statusRoutes
|
|
5409
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
3688
5410
|
invalidateContextStatusCache();
|
|
3689
5411
|
}
|
|
3690
5412
|
return route;
|
|
@@ -3692,10 +5414,18 @@ function parsedProviderModelVersion(id) {
|
|
|
3692
5414
|
async close(reason = 'cli-exit', options = {}) {
|
|
3693
5415
|
const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
|
|
3694
5416
|
closeRequested = true;
|
|
5417
|
+
// Persist any change that is still sitting in the debounce window so a
|
|
5418
|
+
// toggle made right before exit is not lost. Synchronous + best-effort:
|
|
5419
|
+
// teardown must continue even if the final write fails.
|
|
5420
|
+
try { flushConfigSave(); } catch {}
|
|
3695
5421
|
if (channelStartTimer) {
|
|
3696
5422
|
clearTimeout(channelStartTimer);
|
|
3697
5423
|
channelStartTimer = null;
|
|
3698
5424
|
}
|
|
5425
|
+
if (providerSetupWarmupTimer) {
|
|
5426
|
+
clearTimeout(providerSetupWarmupTimer);
|
|
5427
|
+
providerSetupWarmupTimer = null;
|
|
5428
|
+
}
|
|
3699
5429
|
if (providerWarmupTimer) {
|
|
3700
5430
|
clearTimeout(providerWarmupTimer);
|
|
3701
5431
|
providerWarmupTimer = null;
|
|
@@ -3704,9 +5434,21 @@ function parsedProviderModelVersion(id) {
|
|
|
3704
5434
|
clearTimeout(providerModelWarmupTimer);
|
|
3705
5435
|
providerModelWarmupTimer = null;
|
|
3706
5436
|
}
|
|
5437
|
+
if (codeGraphPrewarmTimer) {
|
|
5438
|
+
clearTimeout(codeGraphPrewarmTimer);
|
|
5439
|
+
codeGraphPrewarmTimer = null;
|
|
5440
|
+
}
|
|
5441
|
+
if (statuslineUsageWarmupTimer) {
|
|
5442
|
+
clearTimeout(statuslineUsageWarmupTimer);
|
|
5443
|
+
statuslineUsageWarmupTimer = null;
|
|
5444
|
+
}
|
|
5445
|
+
if (statuslineUsageRefreshTimer) {
|
|
5446
|
+
clearTimeout(statuslineUsageRefreshTimer);
|
|
5447
|
+
statuslineUsageRefreshTimer = null;
|
|
5448
|
+
}
|
|
3707
5449
|
try { cancelBackgroundTasks({ reason, notify: false }); } catch {}
|
|
3708
5450
|
const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
|
|
3709
|
-
try {
|
|
5451
|
+
try { agentTool.closeAll(reason); } catch {}
|
|
3710
5452
|
let mcpStop = null;
|
|
3711
5453
|
try { mcpStop = mcpClient.disconnectAll?.(); } catch {}
|
|
3712
5454
|
const openaiWsStop = globalThis.__mixdogOpenaiWsRuntimeLoaded === true
|
|
@@ -3715,14 +5457,35 @@ function parsedProviderModelVersion(id) {
|
|
|
3715
5457
|
.catch(() => {})
|
|
3716
5458
|
: null;
|
|
3717
5459
|
const patchStop = closePatchRuntimeIfLoaded(detach ? { waitForExit: false } : undefined);
|
|
5460
|
+
const memoryStop = memoryModPromise
|
|
5461
|
+
? memoryModPromise
|
|
5462
|
+
.then((mod) => (typeof mod?.stop === 'function' ? mod.stop() : null))
|
|
5463
|
+
.catch(() => {})
|
|
5464
|
+
.finally(() => {
|
|
5465
|
+
memoryModPromise = null;
|
|
5466
|
+
})
|
|
5467
|
+
: null;
|
|
3718
5468
|
let ok = false;
|
|
3719
5469
|
if (session?.id) {
|
|
3720
5470
|
statusRoutes?.clearGatewaySessionRoute?.(session.id);
|
|
3721
5471
|
ok = mgr.closeSession(session.id, reason);
|
|
3722
5472
|
session = null;
|
|
3723
5473
|
}
|
|
5474
|
+
const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
|
|
5475
|
+
? import('./runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs')
|
|
5476
|
+
.then((mod) => mod?.shutdownShellJobs?.(reason, { sync: !detach }))
|
|
5477
|
+
.catch(() => {})
|
|
5478
|
+
: null;
|
|
5479
|
+
const bashSessionsStop = globalThis.__mixdogBashSessionRuntimeLoaded === true
|
|
5480
|
+
? import('./runtime/agent/orchestrator/tools/bash-session.mjs')
|
|
5481
|
+
.then((mod) => mod?.shutdownBashSessions?.(reason))
|
|
5482
|
+
.catch(() => {})
|
|
5483
|
+
: null;
|
|
3724
5484
|
if (detach) {
|
|
3725
5485
|
try { await withTeardownDeadline(channelStop, 300, false); } catch {}
|
|
5486
|
+
try { await withTeardownDeadline(shellJobsStop, 300, false); } catch {}
|
|
5487
|
+
try { await withTeardownDeadline(bashSessionsStop, 300, false); } catch {}
|
|
5488
|
+
try { await withTeardownDeadline(memoryStop, 1500, false); } catch {}
|
|
3726
5489
|
for (const stop of [mcpStop, openaiWsStop, patchStop]) {
|
|
3727
5490
|
Promise.resolve(stop).catch(() => {});
|
|
3728
5491
|
}
|
|
@@ -3733,6 +5496,9 @@ function parsedProviderModelVersion(id) {
|
|
|
3733
5496
|
withTeardownDeadline(mcpStop, 1500, false),
|
|
3734
5497
|
withTeardownDeadline(openaiWsStop, 1500, false),
|
|
3735
5498
|
withTeardownDeadline(patchStop, 1500, false),
|
|
5499
|
+
withTeardownDeadline(memoryStop, 5500, false),
|
|
5500
|
+
withTeardownDeadline(shellJobsStop, 1500, false),
|
|
5501
|
+
withTeardownDeadline(bashSessionsStop, 1500, false),
|
|
3736
5502
|
]);
|
|
3737
5503
|
return ok;
|
|
3738
5504
|
},
|
|
@@ -3750,7 +5516,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3750
5516
|
const leadish = role === 'lead'
|
|
3751
5517
|
|| sourceType === 'lead'
|
|
3752
5518
|
|| (sourceType === 'cli' && (!sourceName || sourceName === 'main'))
|
|
3753
|
-
|| (!sourceType && !sourceName && owner
|
|
5519
|
+
|| (!sourceType && !sourceName && !isAgentOwner(owner));
|
|
3754
5520
|
if (!leadish) return null;
|
|
3755
5521
|
let preview = cleanSessionPreview(s.preview || '');
|
|
3756
5522
|
let messageCount = Math.max(0, Number(s.messageCount) || 0);
|
|
@@ -3796,11 +5562,11 @@ function parsedProviderModelVersion(id) {
|
|
|
3796
5562
|
await refreshRouteEffort();
|
|
3797
5563
|
session.effort = route.effectiveEffort || null;
|
|
3798
5564
|
session.cwd = currentCwd;
|
|
3799
|
-
applyDeferredToolSurface(session, mode, standaloneTools);
|
|
5565
|
+
applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
3800
5566
|
invalidatePreSessionToolSurface();
|
|
3801
5567
|
invalidateContextStatusCache();
|
|
3802
5568
|
sessionNeedsCwdRefresh = false;
|
|
3803
|
-
statusRoutes
|
|
5569
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
3804
5570
|
return {
|
|
3805
5571
|
id: resumed.id,
|
|
3806
5572
|
messages: resumed.messages || [],
|