mixdog 0.8.0 → 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 +344 -0
- 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 +1223 -95
- 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 +2325 -450
- 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 +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +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/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- 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 +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- 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/advisory-lock.mjs +1 -1
- 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/snapshot-store.mjs +1 -1
- 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/tool-format.mjs +0 -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 +635 -107
- 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 +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- 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 +15 -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 -98
- 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 +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- 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 +617 -368
- 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 +8915 -2418
- 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 +3 -16
- 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;
|
|
@@ -86,6 +168,39 @@ function messageContextText(message) {
|
|
|
86
168
|
return text;
|
|
87
169
|
}
|
|
88
170
|
|
|
171
|
+
function stripSystemReminder(text) {
|
|
172
|
+
return String(text || '')
|
|
173
|
+
.replace(/^\s*<system-reminder>\s*/i, '')
|
|
174
|
+
.replace(/\s*<\/system-reminder>\s*$/i, '')
|
|
175
|
+
.trim();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function splitMarkdownSections(text) {
|
|
179
|
+
const sections = [];
|
|
180
|
+
let current = [];
|
|
181
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
182
|
+
if (/^#\s+/.test(line) && current.length) {
|
|
183
|
+
const body = current.join('\n').trim();
|
|
184
|
+
if (body) sections.push(body);
|
|
185
|
+
current = [line];
|
|
186
|
+
} else {
|
|
187
|
+
current.push(line);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const tail = current.join('\n').trim();
|
|
191
|
+
if (tail) sections.push(tail);
|
|
192
|
+
return sections;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function reminderSectionBucket(section) {
|
|
196
|
+
const heading = String(section.match(/^#\s+([^\n]+)/)?.[1] || '').trim().toLowerCase();
|
|
197
|
+
if (heading.includes('core memory')) return 'memory';
|
|
198
|
+
if (heading.includes('active workflow') || heading.includes('available agents') || heading.includes('workflow')) return 'workflow';
|
|
199
|
+
if (heading.includes('workspace')) return 'workspace';
|
|
200
|
+
if (heading.includes('environment')) return 'environment';
|
|
201
|
+
return 'other';
|
|
202
|
+
}
|
|
203
|
+
|
|
89
204
|
function summarizeContextMessages(messages) {
|
|
90
205
|
const rows = {
|
|
91
206
|
system: { count: 0, tokens: 0 },
|
|
@@ -94,15 +209,54 @@ function summarizeContextMessages(messages) {
|
|
|
94
209
|
tool: { count: 0, tokens: 0 },
|
|
95
210
|
other: { count: 0, tokens: 0 },
|
|
96
211
|
};
|
|
212
|
+
const semantic = {
|
|
213
|
+
system: { count: 0, tokens: 0 },
|
|
214
|
+
chat: { count: 0, tokens: 0 },
|
|
215
|
+
assistant: { count: 0, tokens: 0 },
|
|
216
|
+
toolResults: { count: 0, tokens: 0 },
|
|
217
|
+
reminders: { count: 0, tokens: 0, otherTokens: 0 },
|
|
218
|
+
workflow: { tokens: 0 },
|
|
219
|
+
memory: { tokens: 0 },
|
|
220
|
+
workspace: { tokens: 0 },
|
|
221
|
+
environment: { tokens: 0 },
|
|
222
|
+
other: { tokens: 0 },
|
|
223
|
+
};
|
|
97
224
|
let toolCallCount = 0;
|
|
98
225
|
let toolCallTokens = 0;
|
|
99
226
|
let toolResultCount = 0;
|
|
100
227
|
let toolResultTokens = 0;
|
|
101
228
|
for (const message of messages || []) {
|
|
102
229
|
const role = rows[message?.role] ? message.role : 'other';
|
|
103
|
-
const
|
|
230
|
+
const text = messageContextText(message);
|
|
231
|
+
const tokens = roughTokenCount(text) + 4;
|
|
104
232
|
rows[role].count += 1;
|
|
105
233
|
rows[role].tokens += tokens;
|
|
234
|
+
if (role === 'system') {
|
|
235
|
+
semantic.system.count += 1;
|
|
236
|
+
semantic.system.tokens += tokens;
|
|
237
|
+
} else if (role === 'user') {
|
|
238
|
+
if (String(text || '').trim().startsWith('<system-reminder>')) {
|
|
239
|
+
semantic.reminders.count += 1;
|
|
240
|
+
semantic.reminders.tokens += tokens;
|
|
241
|
+
let sectionTokens = 0;
|
|
242
|
+
for (const section of splitMarkdownSections(stripSystemReminder(text))) {
|
|
243
|
+
const bucket = reminderSectionBucket(section);
|
|
244
|
+
const sectionTokenCount = roughTokenCount(section);
|
|
245
|
+
semantic[bucket].tokens += sectionTokenCount;
|
|
246
|
+
sectionTokens += sectionTokenCount;
|
|
247
|
+
}
|
|
248
|
+
semantic.reminders.otherTokens += Math.max(0, tokens - sectionTokens);
|
|
249
|
+
} else {
|
|
250
|
+
semantic.chat.count += 1;
|
|
251
|
+
semantic.chat.tokens += tokens;
|
|
252
|
+
}
|
|
253
|
+
} else if (role === 'assistant') {
|
|
254
|
+
semantic.assistant.count += 1;
|
|
255
|
+
semantic.assistant.tokens += tokens;
|
|
256
|
+
} else if (role === 'tool') {
|
|
257
|
+
semantic.toolResults.count += 1;
|
|
258
|
+
semantic.toolResults.tokens += tokens;
|
|
259
|
+
}
|
|
106
260
|
if (message?.role === 'assistant' && Array.isArray(message.toolCalls) && message.toolCalls.length) {
|
|
107
261
|
toolCallCount += message.toolCalls.length;
|
|
108
262
|
try { toolCallTokens += roughTokenCount(JSON.stringify(message.toolCalls)); }
|
|
@@ -117,6 +271,7 @@ function summarizeContextMessages(messages) {
|
|
|
117
271
|
count: Array.isArray(messages) ? messages.length : 0,
|
|
118
272
|
estimatedTokens: Array.isArray(messages) ? estimateMessagesTokens(messages) : 0,
|
|
119
273
|
roles: rows,
|
|
274
|
+
semantic,
|
|
120
275
|
toolCallCount,
|
|
121
276
|
toolCallTokens,
|
|
122
277
|
toolResultCount,
|
|
@@ -177,6 +332,10 @@ function envFlag(name) {
|
|
|
177
332
|
return /^(1|true|yes|on)$/i.test(String(process.env[name] || ''));
|
|
178
333
|
}
|
|
179
334
|
|
|
335
|
+
function envPresent(name) {
|
|
336
|
+
return process.env[name] !== undefined && process.env[name] !== '';
|
|
337
|
+
}
|
|
338
|
+
|
|
180
339
|
function envDelayMs(name, fallback, { min = 0, max = 60_000 } = {}) {
|
|
181
340
|
const raw = process.env[name];
|
|
182
341
|
if (raw === undefined || raw === '') return fallback;
|
|
@@ -217,7 +376,7 @@ async function profiledImport(label, spec, { optional = false } = {}) {
|
|
|
217
376
|
const EFFORT_OPTIONS_BY_PROVIDER = {
|
|
218
377
|
openai: ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
219
378
|
'openai-oauth': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
220
|
-
anthropic: ['low', 'medium', 'high', 'max'],
|
|
379
|
+
anthropic: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
221
380
|
'anthropic-oauth': ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
222
381
|
xai: ['none', 'low', 'medium', 'high'],
|
|
223
382
|
'grok-oauth': ['none', 'low', 'medium', 'high'],
|
|
@@ -254,14 +413,15 @@ export const TOOL_SEARCH_TOOL = {
|
|
|
254
413
|
destructiveHint: false,
|
|
255
414
|
idempotentHint: true,
|
|
256
415
|
openWorldHint: false,
|
|
416
|
+
agentHidden: true,
|
|
257
417
|
},
|
|
258
|
-
description: 'Search
|
|
418
|
+
description: 'Search deferred tools; confident queries load matches.',
|
|
259
419
|
inputSchema: {
|
|
260
420
|
type: 'object',
|
|
261
421
|
properties: {
|
|
262
|
-
query: { type: 'string', description: '
|
|
263
|
-
select: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: '
|
|
264
|
-
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.' },
|
|
265
425
|
},
|
|
266
426
|
additionalProperties: false,
|
|
267
427
|
},
|
|
@@ -276,30 +436,52 @@ const CHANNEL_STATUS_TOOL = {
|
|
|
276
436
|
destructiveHint: false,
|
|
277
437
|
idempotentHint: true,
|
|
278
438
|
openWorldHint: false,
|
|
279
|
-
|
|
439
|
+
agentHidden: true,
|
|
280
440
|
},
|
|
281
|
-
description: 'List
|
|
441
|
+
description: 'List channel/schedule/webhook status. No secrets.',
|
|
282
442
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
283
443
|
};
|
|
284
444
|
|
|
285
445
|
const CWD_TOOL = {
|
|
286
446
|
name: 'cwd',
|
|
287
|
-
title: '
|
|
447
|
+
title: 'Work Project',
|
|
288
448
|
annotations: {
|
|
289
|
-
title: '
|
|
449
|
+
title: 'Work Project',
|
|
290
450
|
readOnlyHint: false,
|
|
291
451
|
destructiveHint: false,
|
|
292
452
|
idempotentHint: false,
|
|
293
453
|
openWorldHint: false,
|
|
294
|
-
|
|
454
|
+
agentHidden: true,
|
|
455
|
+
},
|
|
456
|
+
description: 'Show or set the session work project for tool execution.',
|
|
457
|
+
inputSchema: {
|
|
458
|
+
type: 'object',
|
|
459
|
+
properties: {
|
|
460
|
+
action: { type: 'string', enum: ['get', 'set'], description: 'Default get.' },
|
|
461
|
+
path: { type: 'string', description: 'Project directory for set.' },
|
|
462
|
+
},
|
|
463
|
+
additionalProperties: false,
|
|
464
|
+
},
|
|
465
|
+
};
|
|
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,
|
|
295
477
|
},
|
|
296
|
-
description: '
|
|
478
|
+
description: 'Load a named SKILL.md into context.',
|
|
297
479
|
inputSchema: {
|
|
298
480
|
type: 'object',
|
|
299
481
|
properties: {
|
|
300
|
-
|
|
301
|
-
path: { type: 'string', description: 'Directory path for action=set. Relative paths resolve from the current cwd.' },
|
|
482
|
+
name: { type: 'string', description: 'Skill name.' },
|
|
302
483
|
},
|
|
484
|
+
required: ['name'],
|
|
303
485
|
additionalProperties: false,
|
|
304
486
|
},
|
|
305
487
|
};
|
|
@@ -308,11 +490,12 @@ const MEASURED_TOOL_USAGE = Object.freeze({
|
|
|
308
490
|
read: 710,
|
|
309
491
|
code_graph: 520,
|
|
310
492
|
grep: 500,
|
|
493
|
+
find: 480,
|
|
311
494
|
glob: 460,
|
|
312
495
|
list: 430,
|
|
313
496
|
apply_patch: 400,
|
|
314
497
|
explore: 360,
|
|
315
|
-
|
|
498
|
+
agent: 330,
|
|
316
499
|
shell: 81,
|
|
317
500
|
cwd: 2,
|
|
318
501
|
diagnostics: 2,
|
|
@@ -323,44 +506,54 @@ const MEASURED_TOOL_USAGE = Object.freeze({
|
|
|
323
506
|
channel_status: 2,
|
|
324
507
|
});
|
|
325
508
|
const MEASURED_TOOL_ORDER = Object.freeze(Object.keys(MEASURED_TOOL_USAGE));
|
|
326
|
-
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',
|
|
327
520
|
'tool_search',
|
|
328
|
-
'recall',
|
|
329
|
-
'search',
|
|
330
|
-
'web_fetch',
|
|
331
521
|
]);
|
|
332
|
-
const LEAD_DISALLOWED_TOOLS = Object.freeze(['diagnostics', 'open_config']);
|
|
333
|
-
const DEFERRED_DEFAULT_FULL_LIMIT = 8;
|
|
334
522
|
const DEFERRED_DEFAULT_READONLY_TOOLS = Object.freeze([
|
|
335
523
|
'read',
|
|
336
524
|
'code_graph',
|
|
337
525
|
'grep',
|
|
526
|
+
'find',
|
|
338
527
|
'glob',
|
|
339
528
|
'list',
|
|
340
529
|
'explore',
|
|
530
|
+
'Skill',
|
|
341
531
|
'tool_search',
|
|
342
532
|
]);
|
|
343
533
|
const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
|
|
344
534
|
'read',
|
|
345
535
|
'code_graph',
|
|
346
536
|
'grep',
|
|
537
|
+
'find',
|
|
347
538
|
'glob',
|
|
348
539
|
'list',
|
|
349
540
|
'shell',
|
|
350
541
|
'task',
|
|
351
542
|
'explore',
|
|
352
543
|
'apply_patch',
|
|
353
|
-
'
|
|
544
|
+
'agent',
|
|
354
545
|
'recall',
|
|
355
546
|
'search',
|
|
356
547
|
'web_fetch',
|
|
357
548
|
'cwd',
|
|
549
|
+
'Skill',
|
|
358
550
|
'tool_search',
|
|
359
551
|
]);
|
|
360
552
|
const READONLY_TOOL_NAMES = new Set([
|
|
361
553
|
'read',
|
|
362
554
|
'list',
|
|
363
555
|
'grep',
|
|
556
|
+
'find',
|
|
364
557
|
'glob',
|
|
365
558
|
'code_graph',
|
|
366
559
|
'search',
|
|
@@ -371,21 +564,22 @@ const READONLY_TOOL_NAMES = new Set([
|
|
|
371
564
|
'channel_status',
|
|
372
565
|
'schedule_status',
|
|
373
566
|
'fetch',
|
|
567
|
+
'Skill',
|
|
374
568
|
]);
|
|
375
|
-
const
|
|
569
|
+
const AGENT_HIDDEN_WRAPPER_TOOLS = new Set(['search']);
|
|
376
570
|
|
|
377
571
|
function applyStandaloneToolDefaults(tool) {
|
|
378
|
-
if (!tool || !
|
|
572
|
+
if (!tool || !AGENT_HIDDEN_WRAPPER_TOOLS.has(tool.name)) return tool;
|
|
379
573
|
return {
|
|
380
574
|
...tool,
|
|
381
575
|
annotations: {
|
|
382
576
|
...(tool.annotations || {}),
|
|
383
|
-
|
|
577
|
+
agentHidden: true,
|
|
384
578
|
},
|
|
385
579
|
};
|
|
386
580
|
}
|
|
387
581
|
const DEFERRED_SELECT_ALIASES = {
|
|
388
|
-
filesystem: ['read', 'list', 'grep', 'glob'],
|
|
582
|
+
filesystem: ['read', 'list', 'grep', 'find', 'glob'],
|
|
389
583
|
search: ['search', 'web_fetch'],
|
|
390
584
|
web: ['web_fetch', 'search'],
|
|
391
585
|
memory: ['memory', 'recall'],
|
|
@@ -398,11 +592,68 @@ const DEFERRED_SELECT_ALIASES = {
|
|
|
398
592
|
channel: ['channel_status'],
|
|
399
593
|
explore: ['explore'],
|
|
400
594
|
discovery: ['explore'],
|
|
401
|
-
|
|
595
|
+
agent: ['agent'],
|
|
402
596
|
graph: ['code_graph'],
|
|
403
597
|
code: ['code_graph'],
|
|
404
598
|
shell: ['shell', 'task'],
|
|
405
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
|
+
});
|
|
406
657
|
|
|
407
658
|
function normalizeToolMode(mode) {
|
|
408
659
|
const value = String(mode || '').trim().toLowerCase();
|
|
@@ -427,8 +678,21 @@ function effortOptionsFor(provider, model) {
|
|
|
427
678
|
const declared = Array.isArray(model?.reasoningLevels)
|
|
428
679
|
? model.reasoningLevels.map(clean).filter(Boolean)
|
|
429
680
|
: [];
|
|
430
|
-
if (Array.isArray(model?.reasoningLevels)) return filterProvider(declared);
|
|
431
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);
|
|
432
696
|
if (Object.prototype.hasOwnProperty.call(EFFORT_BY_FAMILY, family)) {
|
|
433
697
|
return filterProvider(EFFORT_BY_FAMILY[family]);
|
|
434
698
|
}
|
|
@@ -481,10 +745,23 @@ function toolSpecForMode(mode) {
|
|
|
481
745
|
return mode === 'readonly' ? ['tools:readonly'] : 'full';
|
|
482
746
|
}
|
|
483
747
|
|
|
748
|
+
function deferredSurfaceModeForLead(mode) {
|
|
749
|
+
return mode === 'readonly' ? 'readonly' : 'lead';
|
|
750
|
+
}
|
|
751
|
+
|
|
484
752
|
function clean(value) {
|
|
485
753
|
return String(value ?? '').trim();
|
|
486
754
|
}
|
|
487
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
|
+
|
|
488
765
|
const OUTPUT_STYLE_ORDER = ['default', 'simple', 'extreme-simple'];
|
|
489
766
|
const OUTPUT_STYLE_ALIASES = new Map([
|
|
490
767
|
['compact', 'default'],
|
|
@@ -717,6 +994,21 @@ function resolveRoute(config, { provider, model, effort, fast } = {}) {
|
|
|
717
994
|
};
|
|
718
995
|
}
|
|
719
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
|
+
|
|
720
1012
|
function ensureProviderEnabled(config, provider) {
|
|
721
1013
|
const providers = { ...(config?.providers || {}) };
|
|
722
1014
|
providers[provider] = { ...(providers[provider] || {}), enabled: true };
|
|
@@ -751,12 +1043,51 @@ function normalizeSystemShellCommand(value) {
|
|
|
751
1043
|
function normalizeAutoClearConfig(value = {}) {
|
|
752
1044
|
const raw = value && typeof value === 'object' ? value : {};
|
|
753
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') : '';
|
|
754
1048
|
return {
|
|
755
1049
|
enabled: raw.enabled !== false,
|
|
756
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,
|
|
757
1072
|
};
|
|
758
1073
|
}
|
|
759
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
|
+
|
|
760
1091
|
function formatDurationMs(ms) {
|
|
761
1092
|
const value = Math.max(0, Number(ms) || 0);
|
|
762
1093
|
if (value % 3_600_000 === 0) return `${value / 3_600_000}h`;
|
|
@@ -805,6 +1136,44 @@ function openAiDirectModelSupportsFast(model) {
|
|
|
805
1136
|
|| /^gpt-5\.4-mini(?:-\d{4}|$)/.test(id);
|
|
806
1137
|
}
|
|
807
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
|
+
|
|
808
1177
|
function anthropicModelMetaSupportsFast(model) {
|
|
809
1178
|
const id = clean(model?.id || model).toLowerCase();
|
|
810
1179
|
return /^claude-(opus|sonnet)/.test(id);
|
|
@@ -819,6 +1188,16 @@ function fastCapableFor(provider, model) {
|
|
|
819
1188
|
return false;
|
|
820
1189
|
}
|
|
821
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
|
+
|
|
822
1201
|
function fastPreferenceFor(config, provider, model) {
|
|
823
1202
|
const key = routeFastKey(provider, model);
|
|
824
1203
|
if (!key) return false;
|
|
@@ -827,10 +1206,10 @@ function fastPreferenceFor(config, provider, model) {
|
|
|
827
1206
|
return config?.fastModels?.[key] === true;
|
|
828
1207
|
}
|
|
829
1208
|
|
|
830
|
-
function saveModelSettings(cfgMod, route, { fastCapable = true } = {}) {
|
|
1209
|
+
function saveModelSettings(cfgMod, route, { fastCapable = true, baseConfig = null } = {}) {
|
|
831
1210
|
const key = routeFastKey(route?.provider, route?.model);
|
|
832
|
-
if (!key) return cfgMod.loadConfig();
|
|
833
|
-
const nextConfig = cfgMod.loadConfig();
|
|
1211
|
+
if (!key) return baseConfig || cfgMod.loadConfig();
|
|
1212
|
+
const nextConfig = baseConfig || cfgMod.loadConfig();
|
|
834
1213
|
const modelSettings = { ...(nextConfig.modelSettings || {}) };
|
|
835
1214
|
const nextSetting = { ...(modelSettings[key] || {}) };
|
|
836
1215
|
if (hasOwn(route, 'effort') && route.effort) nextSetting.effort = route.effort;
|
|
@@ -872,17 +1251,80 @@ function routeForStatusline(route) {
|
|
|
872
1251
|
return out;
|
|
873
1252
|
}
|
|
874
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
|
+
|
|
875
1260
|
const ONBOARDING_VERSION = 1;
|
|
876
|
-
const WORKFLOW_ROUTE_SLOTS = ['lead', '
|
|
1261
|
+
const WORKFLOW_ROUTE_SLOTS = ['lead', 'agent', 'explorer', 'memory'];
|
|
877
1262
|
const FIXED_AGENT_SLOTS = Object.freeze([
|
|
878
1263
|
{ id: 'explore', label: 'Explore', description: 'Broad repository exploration', workflowSlot: 'explorer' },
|
|
879
|
-
{ id: 'web-researcher', label: 'Web Researcher', description: 'External current-info research' },
|
|
880
1264
|
{ id: 'maintainer', label: 'Maintainer', description: 'Background memory and upkeep', workflowSlot: 'memory' },
|
|
881
1265
|
{ id: 'worker', label: 'Worker', description: 'Scoped implementation' },
|
|
882
1266
|
{ id: 'heavy-worker', label: 'Heavy Worker', description: 'Broad or multi-file implementation' },
|
|
883
1267
|
{ id: 'reviewer', label: 'Reviewer', description: 'Diff review and risk checks' },
|
|
884
1268
|
{ id: 'debugger', label: 'Debugger', description: 'Root-cause analysis and failure tracing' },
|
|
885
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
|
+
});
|
|
886
1328
|
const AGENT_ROLE_IDS = new Set(FIXED_AGENT_SLOTS.map((agent) => agent.id));
|
|
887
1329
|
const agentDefinitionCache = new Map();
|
|
888
1330
|
const DEFAULT_WORKFLOW_ID = 'default';
|
|
@@ -940,12 +1382,14 @@ function readWorkflowPackFromDir(dir, source = 'built-in') {
|
|
|
940
1382
|
const entry = clean(manifest.entry) || 'WORKFLOW.md';
|
|
941
1383
|
const body = readTextSafe(join(dir, entry));
|
|
942
1384
|
if (!body) return null;
|
|
1385
|
+
const agentsConfigured = Array.isArray(manifest.agents);
|
|
943
1386
|
return {
|
|
944
1387
|
id,
|
|
945
1388
|
name: clean(manifest.name) || id,
|
|
946
1389
|
description: clean(manifest.description),
|
|
947
1390
|
entry,
|
|
948
|
-
|
|
1391
|
+
agentsConfigured,
|
|
1392
|
+
agents: agentsConfigured ? manifest.agents.map((agent) => normalizeAgentId(agent) || normalizeWorkflowId(agent)).filter(Boolean) : [],
|
|
949
1393
|
body,
|
|
950
1394
|
source,
|
|
951
1395
|
};
|
|
@@ -983,6 +1427,20 @@ function loadWorkflowPack(dataDir, id) {
|
|
|
983
1427
|
return readWorkflowPackFromDir(join(STANDALONE_ROOT, 'workflows', DEFAULT_WORKFLOW_ID), 'built-in');
|
|
984
1428
|
}
|
|
985
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
|
+
|
|
986
1444
|
function loadAgentDefinition(dataDir, id) {
|
|
987
1445
|
const agentId = normalizeAgentId(id) || normalizeWorkflowId(id);
|
|
988
1446
|
if (!agentId) return null;
|
|
@@ -991,20 +1449,22 @@ function loadAgentDefinition(dataDir, id) {
|
|
|
991
1449
|
for (const dir of agentSourceDirs(dataDir, agentId)) {
|
|
992
1450
|
const manifest = readJsonSafe(join(dir, 'agent.json')) || {};
|
|
993
1451
|
const entry = clean(manifest.entry) || 'AGENT.md';
|
|
994
|
-
const
|
|
1452
|
+
const doc = readMarkdownDocument(readTextSafe(join(dir, entry)));
|
|
1453
|
+
const body = doc.body;
|
|
995
1454
|
if (!body) continue;
|
|
996
1455
|
const definition = {
|
|
997
1456
|
id: agentId,
|
|
998
1457
|
name: clean(manifest.name) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
|
|
999
1458
|
description: clean(manifest.description) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.description || '',
|
|
1459
|
+
permission: normalizeAgentPermissionOrNone(doc.frontmatter.permission),
|
|
1460
|
+
frontmatter: doc.frontmatter,
|
|
1000
1461
|
body,
|
|
1001
1462
|
};
|
|
1002
1463
|
agentDefinitionCache.set(cacheKey, definition);
|
|
1003
1464
|
return definition;
|
|
1004
1465
|
}
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
if (!legacyBody) {
|
|
1466
|
+
const legacyDoc = readMarkdownDocument(readTextSafe(join(STANDALONE_ROOT, 'agents', `${agentId}.md`)));
|
|
1467
|
+
if (!legacyDoc.body) {
|
|
1008
1468
|
agentDefinitionCache.set(cacheKey, null);
|
|
1009
1469
|
return null;
|
|
1010
1470
|
}
|
|
@@ -1012,7 +1472,9 @@ function loadAgentDefinition(dataDir, id) {
|
|
|
1012
1472
|
id: agentId,
|
|
1013
1473
|
name: FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
|
|
1014
1474
|
description: '',
|
|
1015
|
-
|
|
1475
|
+
permission: normalizeAgentPermissionOrNone(legacyDoc.frontmatter.permission),
|
|
1476
|
+
frontmatter: legacyDoc.frontmatter,
|
|
1477
|
+
body: legacyDoc.body,
|
|
1016
1478
|
};
|
|
1017
1479
|
agentDefinitionCache.set(cacheKey, definition);
|
|
1018
1480
|
return definition;
|
|
@@ -1027,7 +1489,7 @@ function workflowContextBlock(config, dataDir) {
|
|
|
1027
1489
|
if (pack.description) lines.push(pack.description);
|
|
1028
1490
|
lines.push(pack.body);
|
|
1029
1491
|
|
|
1030
|
-
const agentIds = pack.
|
|
1492
|
+
const agentIds = pack.agentsConfigured ? pack.agents : FIXED_AGENT_SLOTS.map((agent) => agent.id);
|
|
1031
1493
|
const agentBlocks = agentIds
|
|
1032
1494
|
.map((id) => loadAgentDefinition(dataDir, id))
|
|
1033
1495
|
.filter(Boolean);
|
|
@@ -1046,6 +1508,11 @@ function normalizeWorkflowRoute(routeLike, fallback = {}) {
|
|
|
1046
1508
|
const provider = clean(routeLike?.provider) || clean(fallback.provider);
|
|
1047
1509
|
const model = clean(routeLike?.model) || clean(fallback.model);
|
|
1048
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;
|
|
1049
1516
|
const effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
|
|
1050
1517
|
const fast = routeLike?.fast ?? fallback.fast;
|
|
1051
1518
|
return {
|
|
@@ -1056,6 +1523,41 @@ function normalizeWorkflowRoute(routeLike, fallback = {}) {
|
|
|
1056
1523
|
};
|
|
1057
1524
|
}
|
|
1058
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
|
+
|
|
1059
1561
|
function upsertWorkflowPreset(presets, slot, routeLike) {
|
|
1060
1562
|
const route = normalizeWorkflowRoute(routeLike);
|
|
1061
1563
|
if (!route) return presets;
|
|
@@ -1063,7 +1565,7 @@ function upsertWorkflowPreset(presets, slot, routeLike) {
|
|
|
1063
1565
|
const preset = {
|
|
1064
1566
|
id,
|
|
1065
1567
|
name: workflowPresetName(slot),
|
|
1066
|
-
type: '
|
|
1568
|
+
type: 'agent',
|
|
1067
1569
|
provider: route.provider,
|
|
1068
1570
|
model: route.model,
|
|
1069
1571
|
...(route.effort ? { effort: route.effort } : {}),
|
|
@@ -1085,24 +1587,19 @@ function summarizeWorkflowRoutes(config) {
|
|
|
1085
1587
|
return out;
|
|
1086
1588
|
}
|
|
1087
1589
|
|
|
1088
|
-
function
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
} catch {
|
|
1096
|
-
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;
|
|
1097
1597
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
function routeFromPreset(config, presetName) {
|
|
1101
|
-
const preset = findPreset(config, presetName);
|
|
1598
|
+
const preset = findPreset(config, slotValue);
|
|
1102
1599
|
return preset ? normalizeWorkflowRoute(preset) : null;
|
|
1103
1600
|
}
|
|
1104
1601
|
|
|
1105
|
-
function agentRouteFromConfig(config, agentId,
|
|
1602
|
+
function agentRouteFromConfig(config, agentId, _dataDir) {
|
|
1106
1603
|
const id = normalizeAgentId(agentId);
|
|
1107
1604
|
if (!id) return null;
|
|
1108
1605
|
const explicit = normalizeWorkflowRoute(config?.agents?.[id])
|
|
@@ -1118,7 +1615,7 @@ function agentRouteFromConfig(config, agentId, dataDir) {
|
|
|
1118
1615
|
if (id === 'explore') return routeFromPreset(config, config?.maintenance?.explore);
|
|
1119
1616
|
if (id === 'maintainer') return routeFromPreset(config, config?.maintenance?.memory);
|
|
1120
1617
|
|
|
1121
|
-
return
|
|
1618
|
+
return null;
|
|
1122
1619
|
}
|
|
1123
1620
|
|
|
1124
1621
|
function toolResponseText(result) {
|
|
@@ -1157,21 +1654,57 @@ function currentSessionRecallRows(session, query, { limit = 10 } = {}) {
|
|
|
1157
1654
|
|
|
1158
1655
|
function parseToolSelection(value) {
|
|
1159
1656
|
if (Array.isArray(value)) return value.map(clean).filter(Boolean);
|
|
1160
|
-
|
|
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, '')
|
|
1161
1661
|
.split(/[,\s]+/)
|
|
1162
1662
|
.map(clean)
|
|
1163
1663
|
.filter(Boolean);
|
|
1164
1664
|
}
|
|
1165
1665
|
|
|
1666
|
+
function parseToolSearchQuerySelection(query) {
|
|
1667
|
+
const match = clean(query).match(/^select\s*:\s*(.+)$/i);
|
|
1668
|
+
return match ? parseToolSelection(match[1]) : [];
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1166
1671
|
function toolKind(tool) {
|
|
1167
1672
|
const name = clean(tool?.name);
|
|
1168
1673
|
if (name.startsWith('mcp__')) return 'mcp';
|
|
1169
|
-
if (name.startsWith('
|
|
1170
|
-
if (
|
|
1171
|
-
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';
|
|
1677
|
+
if (['apply_patch', 'shell'].includes(name)) return 'mutation';
|
|
1172
1678
|
return 'tool';
|
|
1173
1679
|
}
|
|
1174
1680
|
|
|
1681
|
+
function toolSchemaBucket(tool) {
|
|
1682
|
+
const name = clean(tool?.name);
|
|
1683
|
+
const kind = toolKind(tool);
|
|
1684
|
+
if (kind === 'mcp') return 'mcp';
|
|
1685
|
+
if (kind === 'skill') return 'skills';
|
|
1686
|
+
if (name === 'memory' || name === 'recall' || name.includes('memory')) return 'memory';
|
|
1687
|
+
if (name === 'search' || name === 'web_fetch') return 'web';
|
|
1688
|
+
if (['read', 'grep', 'find', 'glob', 'list', 'code_graph', 'explore'].includes(name)) return 'code';
|
|
1689
|
+
if (['shell', 'apply_patch'].includes(name)) return 'mutation';
|
|
1690
|
+
if (name === 'agent' || name === 'delegate') return 'agents';
|
|
1691
|
+
if (name.includes('channel') || name.includes('discord') || name.includes('webhook')) return 'channels';
|
|
1692
|
+
if (name.includes('provider') || name === 'tool_search' || name === 'cwd') return 'setup';
|
|
1693
|
+
return 'other';
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function estimateToolSchemaBreakdown(tools) {
|
|
1697
|
+
const out = {};
|
|
1698
|
+
for (const tool of Array.isArray(tools) ? tools : []) {
|
|
1699
|
+
const bucket = toolSchemaBucket(tool);
|
|
1700
|
+
const row = out[bucket] || { count: 0, tokens: 0 };
|
|
1701
|
+
row.count += 1;
|
|
1702
|
+
row.tokens += estimateToolSchemaTokens([tool]);
|
|
1703
|
+
out[bucket] = row;
|
|
1704
|
+
}
|
|
1705
|
+
return out;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1175
1708
|
function measuredToolUsage(name) {
|
|
1176
1709
|
return MEASURED_TOOL_USAGE[clean(name)] || 0;
|
|
1177
1710
|
}
|
|
@@ -1201,6 +1734,17 @@ function activeToolForSurface(tool) {
|
|
|
1201
1734
|
return JSON.parse(JSON.stringify(tool));
|
|
1202
1735
|
}
|
|
1203
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
|
+
|
|
1204
1748
|
function filterDisallowedTools(tools, disallowed = []) {
|
|
1205
1749
|
if (!Array.isArray(disallowed) || disallowed.length === 0) return tools;
|
|
1206
1750
|
const deny = new Set(disallowed.map((name) => clean(name)).filter(Boolean));
|
|
@@ -1221,24 +1765,14 @@ function sortedNamesByMeasuredUsage(names) {
|
|
|
1221
1765
|
}
|
|
1222
1766
|
|
|
1223
1767
|
export function defaultDeferredToolNames(catalog, mode) {
|
|
1768
|
+
const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
1224
1769
|
if (mode === 'lead') {
|
|
1225
|
-
const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
1226
1770
|
return new Set(DEFERRED_DEFAULT_LEAD_TOOLS.filter((name) => available.has(name)));
|
|
1227
1771
|
}
|
|
1228
1772
|
if (mode === 'readonly') {
|
|
1229
|
-
const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
1230
1773
|
return new Set(DEFERRED_DEFAULT_READONLY_TOOLS.filter((name) => available.has(name)));
|
|
1231
1774
|
}
|
|
1232
|
-
|
|
1233
|
-
const limit = DEFERRED_DEFAULT_FULL_LIMIT;
|
|
1234
|
-
for (const tool of sortedCatalogByMeasuredUsage(catalog)) {
|
|
1235
|
-
const name = clean(tool?.name);
|
|
1236
|
-
if (!name || names.has(name)) continue;
|
|
1237
|
-
if (mode === 'readonly' && !isReadonlySelectable(tool)) continue;
|
|
1238
|
-
names.add(name);
|
|
1239
|
-
if (names.size >= DEFERRED_ALWAYS_ACTIVE_TOOLS.size + limit) break;
|
|
1240
|
-
}
|
|
1241
|
-
return names;
|
|
1775
|
+
return new Set(DEFERRED_DEFAULT_FULL_TOOLS.filter((name) => available.has(name)));
|
|
1242
1776
|
}
|
|
1243
1777
|
|
|
1244
1778
|
export function compactToolSearchDescription(value, max = 220) {
|
|
@@ -1257,25 +1791,180 @@ function toolRow(tool, activeNames = new Set()) {
|
|
|
1257
1791
|
};
|
|
1258
1792
|
}
|
|
1259
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
|
+
|
|
1260
1832
|
function toolSearchTokens(value) {
|
|
1261
1833
|
return (clean(value).toLowerCase().match(/[a-z0-9_.-]+/g) || [])
|
|
1262
1834
|
.map((token) => token.replace(/[-.]+/g, '_'))
|
|
1263
1835
|
.filter(Boolean);
|
|
1264
1836
|
}
|
|
1265
1837
|
|
|
1838
|
+
function toolSearchMeaningfulTokens(value) {
|
|
1839
|
+
return toolSearchTokens(value).filter((token) => !TOOL_SEARCH_STOP_WORDS.has(token));
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1266
1842
|
function toolSearchText(row) {
|
|
1267
1843
|
const text = `${row.name} ${String(row.name || '').replace(/_/g, ' ')} ${row.kind} ${row.description} ${row.active ? 'active' : 'deferred'}`;
|
|
1268
1844
|
return `${text} ${text.replace(/[-.]+/g, '_')}`.toLowerCase();
|
|
1269
1845
|
}
|
|
1270
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
|
+
|
|
1271
1910
|
function toolSearchMatches(row, query) {
|
|
1272
1911
|
const raw = clean(query).toLowerCase();
|
|
1273
1912
|
if (!raw) return true;
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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 [];
|
|
1279
1968
|
}
|
|
1280
1969
|
|
|
1281
1970
|
function expandSelectionNames(names) {
|
|
@@ -1290,6 +1979,39 @@ function expandSelectionNames(names) {
|
|
|
1290
1979
|
return [...new Set(out)];
|
|
1291
1980
|
}
|
|
1292
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
|
+
|
|
1293
2015
|
function isReadonlySelectable(tool) {
|
|
1294
2016
|
const name = clean(tool?.name);
|
|
1295
2017
|
if (READONLY_TOOL_NAMES.has(name)) return true;
|
|
@@ -1299,8 +2021,9 @@ function isReadonlySelectable(tool) {
|
|
|
1299
2021
|
return false;
|
|
1300
2022
|
}
|
|
1301
2023
|
|
|
1302
|
-
function applyDeferredToolSurface(session, mode, extraTools = []) {
|
|
2024
|
+
function applyDeferredToolSurface(session, mode, extraTools = [], options = {}) {
|
|
1303
2025
|
if (!session || !Array.isArray(session.tools)) return session;
|
|
2026
|
+
const providerMode = deferredProviderMode(options.provider || session.provider);
|
|
1304
2027
|
const byName = new Map();
|
|
1305
2028
|
for (const tool of [...session.tools, ...(extraTools || [])]) {
|
|
1306
2029
|
const name = clean(tool?.name);
|
|
@@ -1309,14 +2032,34 @@ function applyDeferredToolSurface(session, mode, extraTools = []) {
|
|
|
1309
2032
|
}
|
|
1310
2033
|
const catalog = sortedCatalogByMeasuredUsage([...byName.values()]);
|
|
1311
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);
|
|
1312
2044
|
session.deferredToolCatalog = catalog;
|
|
1313
2045
|
session.deferredToolUsage = MEASURED_TOOL_USAGE;
|
|
1314
|
-
session.
|
|
2046
|
+
session.deferredDefaultTools = sortedNamesByMeasuredUsage(defaultNames);
|
|
2047
|
+
session.deferredProviderMode = providerMode;
|
|
2048
|
+
session.deferredNativeTools = providerMode === 'native';
|
|
1315
2049
|
session.tools.length = 0;
|
|
2050
|
+
const active = [];
|
|
1316
2051
|
for (const tool of catalog) {
|
|
1317
|
-
if (!
|
|
2052
|
+
if (!selected.has(clean(tool?.name))) continue;
|
|
1318
2053
|
if (mode === 'readonly' && !isReadonlySelectable(tool)) continue;
|
|
1319
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);
|
|
1320
2063
|
}
|
|
1321
2064
|
return session;
|
|
1322
2065
|
}
|
|
@@ -1325,57 +2068,106 @@ function selectDeferredTools(session, names, mode) {
|
|
|
1325
2068
|
const catalog = Array.isArray(session?.deferredToolCatalog)
|
|
1326
2069
|
? session.deferredToolCatalog
|
|
1327
2070
|
: (Array.isArray(session?.tools) ? session.tools : []);
|
|
1328
|
-
const active = new Set((session?.tools || []).map((tool) => tool?.name).filter(Boolean));
|
|
1329
|
-
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
|
+
}
|
|
1330
2081
|
const added = [];
|
|
1331
2082
|
const already = [];
|
|
1332
2083
|
const blocked = [];
|
|
1333
2084
|
const missing = [];
|
|
1334
|
-
for (const
|
|
1335
|
-
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);
|
|
1336
2089
|
if (!tool) {
|
|
1337
|
-
missing.push(
|
|
2090
|
+
missing.push(requestedName);
|
|
1338
2091
|
continue;
|
|
1339
2092
|
}
|
|
1340
2093
|
if (mode === 'readonly' && !isReadonlySelectable(tool)) {
|
|
1341
2094
|
blocked.push({ name, reason: 'readonly mode' });
|
|
1342
2095
|
continue;
|
|
1343
2096
|
}
|
|
1344
|
-
if (active.has(name)) {
|
|
2097
|
+
if (active.has(name) || discovered.has(name)) {
|
|
1345
2098
|
already.push(name);
|
|
1346
2099
|
continue;
|
|
1347
2100
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
2101
|
+
if (native) {
|
|
2102
|
+
discovered.add(name);
|
|
2103
|
+
} else {
|
|
2104
|
+
session.tools.push(tool);
|
|
2105
|
+
active.add(name);
|
|
2106
|
+
}
|
|
1350
2107
|
added.push(name);
|
|
1351
2108
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
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 };
|
|
1354
2116
|
}
|
|
1355
2117
|
|
|
1356
2118
|
function renderToolSearch(args = {}, session, mode = 'full') {
|
|
1357
2119
|
const catalog = Array.isArray(session?.deferredToolCatalog)
|
|
1358
2120
|
? session.deferredToolCatalog
|
|
1359
2121
|
: (Array.isArray(session?.tools) ? session.tools : []);
|
|
1360
|
-
const
|
|
1361
|
-
const
|
|
1362
|
-
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();
|
|
1363
2127
|
const limit = Math.max(1, Math.min(50, Number(args.limit) || 20));
|
|
1364
|
-
const
|
|
1365
|
-
const
|
|
1366
|
-
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);
|
|
1367
2140
|
const matches = query
|
|
1368
|
-
? rows
|
|
2141
|
+
? rankedToolSearchRows(rows, query)
|
|
1369
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;
|
|
1370
2152
|
return JSON.stringify({
|
|
1371
|
-
selected
|
|
2153
|
+
selected,
|
|
2154
|
+
...(nativeToolSearch ? { nativeToolSearch } : {}),
|
|
1372
2155
|
totalMatches: matches.length,
|
|
1373
2156
|
matches: matches.slice(0, limit),
|
|
1374
2157
|
activeTools: sortedNamesByMeasuredUsage(nextActiveNames),
|
|
1375
|
-
|
|
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.',
|
|
1376
2160
|
}, null, 2);
|
|
1377
2161
|
}
|
|
1378
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
|
+
|
|
1379
2171
|
function resolveCwdPath(currentCwd, value) {
|
|
1380
2172
|
const raw = clean(value);
|
|
1381
2173
|
if (!raw) throw new Error('cwd: path is required for action=set');
|
|
@@ -1398,7 +2190,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1398
2190
|
rootDir: STANDALONE_ROOT,
|
|
1399
2191
|
dataDir: STANDALONE_DATA_DIR,
|
|
1400
2192
|
});
|
|
1401
|
-
ensureProjectMixdogMd({ cwd });
|
|
1402
2193
|
bootProfile('standalone-env:ready', { ms: (performance.now() - standaloneStartedAt).toFixed(1) });
|
|
1403
2194
|
|
|
1404
2195
|
const importsStartedAt = performance.now();
|
|
@@ -1430,18 +2221,51 @@ export async function createMixdogSessionRuntime({
|
|
|
1430
2221
|
profiledImport('code-graph-tool-defs', CODE_GRAPH_TOOL_DEFS, { optional: true }),
|
|
1431
2222
|
]);
|
|
1432
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
|
+
});
|
|
1433
2230
|
let memoryModPromise = null;
|
|
1434
|
-
let memoryInitPromise = null;
|
|
1435
2231
|
let searchModPromise = null;
|
|
1436
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
|
+
};
|
|
1437
2261
|
|
|
1438
2262
|
async function getMemoryModule() {
|
|
2263
|
+
if (!memoryEnabled()) throw new Error('memory is disabled in settings');
|
|
1439
2264
|
const startedAt = performance.now();
|
|
1440
|
-
memoryModPromise ??=
|
|
2265
|
+
memoryModPromise ??= Promise.resolve(memoryRuntime);
|
|
1441
2266
|
const mod = await memoryModPromise;
|
|
1442
2267
|
if (typeof mod?.init === 'function') {
|
|
1443
|
-
|
|
1444
|
-
await memoryInitPromise;
|
|
2268
|
+
await mod.init();
|
|
1445
2269
|
}
|
|
1446
2270
|
bootProfile('memory-runtime:ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
1447
2271
|
return mod;
|
|
@@ -1455,6 +2279,211 @@ export async function createMixdogSessionRuntime({
|
|
|
1455
2279
|
return mod;
|
|
1456
2280
|
}
|
|
1457
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
|
+
|
|
1458
2487
|
async function getCodeGraphModule() {
|
|
1459
2488
|
const startedAt = performance.now();
|
|
1460
2489
|
codeGraphModPromise ??= import(CODE_GRAPH_RUNTIME);
|
|
@@ -1475,8 +2504,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1475
2504
|
};
|
|
1476
2505
|
nextConfig.default = workflowPresetId('lead');
|
|
1477
2506
|
|
|
1478
|
-
|
|
1479
|
-
config = nextConfig;
|
|
2507
|
+
saveConfigAndAdopt(nextConfig);
|
|
1480
2508
|
return leadRoute;
|
|
1481
2509
|
}
|
|
1482
2510
|
|
|
@@ -1517,6 +2545,10 @@ export async function createMixdogSessionRuntime({
|
|
|
1517
2545
|
async function loadCoreMemoryContext() {
|
|
1518
2546
|
// Boot should not pay for memory/PG startup unless explicitly requested.
|
|
1519
2547
|
// Recall and memory tools still initialize the memory service on first use.
|
|
2548
|
+
if (!memoryEnabled()) {
|
|
2549
|
+
bootProfile('core-memory:disabled');
|
|
2550
|
+
return '';
|
|
2551
|
+
}
|
|
1520
2552
|
if (process.env.MIXDOG_BOOT_CORE_MEMORY !== '1') {
|
|
1521
2553
|
bootProfile('core-memory:skipped');
|
|
1522
2554
|
return '';
|
|
@@ -1549,32 +2581,100 @@ export async function createMixdogSessionRuntime({
|
|
|
1549
2581
|
setConfiguredShell(normalizeSystemShellConfig(config.shell).command);
|
|
1550
2582
|
let configHasSecrets = false;
|
|
1551
2583
|
let route = resolveRoute(config, { provider, model });
|
|
2584
|
+
let searchRoute = normalizeSearchRouteConfig(config.searchRoute);
|
|
1552
2585
|
bootProfile('config:ready', { ms: (performance.now() - configStartedAt).toFixed(1) });
|
|
1553
2586
|
let mode = normalizeToolMode(toolMode);
|
|
1554
2587
|
let session = null;
|
|
1555
2588
|
let sessionCreatePromise = null;
|
|
1556
2589
|
let currentCwd = cwd;
|
|
1557
2590
|
let sessionNeedsCwdRefresh = false;
|
|
1558
|
-
const workspaceRouter = createWorkspaceRouter({ entryCwd: cwd });
|
|
1559
2591
|
let closeRequested = false;
|
|
1560
2592
|
let channelStartTimer = null;
|
|
2593
|
+
let providerSetupWarmupTimer = null;
|
|
1561
2594
|
let providerWarmupTimer = null;
|
|
1562
2595
|
let providerModelWarmupTimer = null;
|
|
2596
|
+
let codeGraphPrewarmTimer = null;
|
|
2597
|
+
let codeGraphPrewarmInFlight = false;
|
|
2598
|
+
let codeGraphPrewarmQueuedCwd = '';
|
|
2599
|
+
let statuslineUsageWarmupTimer = null;
|
|
2600
|
+
let statuslineUsageRefreshTimer = null;
|
|
1563
2601
|
let activeTurnCount = 0;
|
|
1564
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
|
+
}
|
|
1565
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 });
|
|
1566
2625
|
const providerWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_WARMUP_DELAY_MS', 1_500, { min: 0, max: 60_000 });
|
|
1567
|
-
|
|
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 });
|
|
1568
2638
|
const channelStartDelayMs = envDelayMs('MIXDOG_CHANNEL_START_DELAY_MS', 10_000, { min: 0, max: 120_000 });
|
|
1569
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;
|
|
1570
2666
|
const modelMetaByRoute = new Map();
|
|
1571
2667
|
const notificationListeners = new Set();
|
|
1572
2668
|
let providerModelsCache = { models: null, at: 0 };
|
|
1573
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;
|
|
1574
2674
|
let providerSetupCache = { setup: null, at: 0 };
|
|
2675
|
+
let providerSetupQuickCache = { setup: null, at: 0 };
|
|
1575
2676
|
let providerSetupPromise = null;
|
|
1576
2677
|
let providerInitPromise = null;
|
|
1577
|
-
const PROVIDER_SETUP_CACHE_TTL_MS = 10_000;
|
|
1578
2678
|
let mcpFailures = [];
|
|
1579
2679
|
let preSessionToolSurface = null;
|
|
1580
2680
|
let contextStatusCacheKey = null;
|
|
@@ -1584,72 +2684,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1584
2684
|
hooks.emit('runtime:start', { cwd: currentCwd, provider: route.provider, model: route.model, toolMode: mode });
|
|
1585
2685
|
bootProfile('hooks:ready', { ms: (performance.now() - hooksStartedAt).toFixed(1) });
|
|
1586
2686
|
|
|
1587
|
-
function contextContentLength(content) {
|
|
1588
|
-
if (typeof content === 'string') return content.length;
|
|
1589
|
-
if (!Array.isArray(content)) {
|
|
1590
|
-
try { return JSON.stringify(content ?? '').length; } catch { return String(content ?? '').length; }
|
|
1591
|
-
}
|
|
1592
|
-
let length = 0;
|
|
1593
|
-
for (const part of content) {
|
|
1594
|
-
if (typeof part === 'string') length += part.length;
|
|
1595
|
-
else if (typeof part?.text === 'string') length += part.text.length;
|
|
1596
|
-
else {
|
|
1597
|
-
try { length += JSON.stringify(part ?? '').length; } catch { length += String(part ?? '').length; }
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
return length;
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
function sameContextStatusKey(a, b) {
|
|
1604
|
-
if (!a || !b || a.length !== b.length) return false;
|
|
1605
|
-
for (let i = 0; i < a.length; i++) {
|
|
1606
|
-
if (!Object.is(a[i], b[i])) return false;
|
|
1607
|
-
}
|
|
1608
|
-
return true;
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
function buildContextStatusCacheKey(messages, tools, bridgeMode) {
|
|
1612
|
-
const lastMessage = messages[messages.length - 1] || null;
|
|
1613
|
-
const compaction = session?.compaction || null;
|
|
1614
|
-
return [
|
|
1615
|
-
session?.id || null,
|
|
1616
|
-
route.provider,
|
|
1617
|
-
route.model,
|
|
1618
|
-
currentCwd,
|
|
1619
|
-
mode,
|
|
1620
|
-
bridgeMode,
|
|
1621
|
-
messages,
|
|
1622
|
-
messages.length,
|
|
1623
|
-
lastMessage,
|
|
1624
|
-
lastMessage?.role || null,
|
|
1625
|
-
lastMessage?.content || null,
|
|
1626
|
-
contextContentLength(lastMessage?.content),
|
|
1627
|
-
Array.isArray(lastMessage?.toolCalls) ? lastMessage.toolCalls.length : 0,
|
|
1628
|
-
tools,
|
|
1629
|
-
tools.length,
|
|
1630
|
-
session?.contextWindow || null,
|
|
1631
|
-
session?.rawContextWindow || null,
|
|
1632
|
-
session?.effectiveContextWindowPercent || null,
|
|
1633
|
-
session?.lastContextTokens || 0,
|
|
1634
|
-
session?.lastContextTokensUpdatedAt || 0,
|
|
1635
|
-
session?.lastContextTokensStaleAfterCompact === true,
|
|
1636
|
-
session?.lastInputTokens || 0,
|
|
1637
|
-
session?.lastOutputTokens || 0,
|
|
1638
|
-
session?.lastCachedReadTokens || 0,
|
|
1639
|
-
session?.lastCacheWriteTokens || 0,
|
|
1640
|
-
session?.totalInputTokens || 0,
|
|
1641
|
-
session?.totalOutputTokens || 0,
|
|
1642
|
-
session?.totalCachedReadTokens || 0,
|
|
1643
|
-
session?.totalCacheWriteTokens || 0,
|
|
1644
|
-
session?.compactBoundaryTokens || 0,
|
|
1645
|
-
compaction,
|
|
1646
|
-
compaction?.lastChangedAt || 0,
|
|
1647
|
-
compaction?.lastCompactAt || 0,
|
|
1648
|
-
compaction?.boundaryTokens || 0,
|
|
1649
|
-
compaction?.triggerTokens || 0,
|
|
1650
|
-
];
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
2687
|
function mcpTransportLabel(cfg = {}) {
|
|
1654
2688
|
if (cfg.autoDetect) return `autoDetect:${cfg.autoDetect}`;
|
|
1655
2689
|
if (cfg.transport === 'http' || cfg.url) return 'http';
|
|
@@ -1659,23 +2693,42 @@ export async function createMixdogSessionRuntime({
|
|
|
1659
2693
|
|
|
1660
2694
|
function emitRuntimeNotification(content, meta = {}) {
|
|
1661
2695
|
const text = String(content || '').trim();
|
|
1662
|
-
if (!text) return;
|
|
2696
|
+
if (!text) return false;
|
|
1663
2697
|
const event = { content: text, meta: meta && typeof meta === 'object' ? meta : {} };
|
|
2698
|
+
let handled = false;
|
|
1664
2699
|
for (const listener of [...notificationListeners]) {
|
|
1665
|
-
try {
|
|
2700
|
+
try {
|
|
2701
|
+
if (listener(event) === true) handled = true;
|
|
2702
|
+
} catch {}
|
|
1666
2703
|
}
|
|
2704
|
+
return handled;
|
|
1667
2705
|
}
|
|
1668
2706
|
|
|
1669
2707
|
function notifyFnForSession(callerSessionId) {
|
|
1670
2708
|
return (text, meta = {}) => {
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1673
|
-
// TUI sessions
|
|
1674
|
-
// onNotification
|
|
1675
|
-
//
|
|
1676
|
-
|
|
1677
|
-
|
|
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 {}
|
|
1678
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 {}
|
|
2730
|
+
}
|
|
2731
|
+
return enqueued || handledByRuntimeListener;
|
|
1679
2732
|
};
|
|
1680
2733
|
}
|
|
1681
2734
|
|
|
@@ -1737,40 +2790,93 @@ export async function createMixdogSessionRuntime({
|
|
|
1737
2790
|
};
|
|
1738
2791
|
}
|
|
1739
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
|
+
|
|
1740
2843
|
function applyResolvedCwd(nextCwd, { markRefresh = true } = {}) {
|
|
1741
2844
|
const resolved = resolve(nextCwd);
|
|
1742
2845
|
const stat = statSync(resolved);
|
|
1743
2846
|
if (!stat.isDirectory()) throw new Error(`cwd: not a directory: ${resolved}`);
|
|
1744
2847
|
const changed = resolve(currentCwd) !== resolved;
|
|
1745
2848
|
currentCwd = resolved;
|
|
1746
|
-
ensureProjectMixdogMd({ cwd: currentCwd });
|
|
1747
2849
|
process.env.MIXDOG_SESSION_CWD = currentCwd;
|
|
1748
2850
|
writeLastSessionCwd(currentCwd);
|
|
1749
2851
|
if (session) session.cwd = currentCwd;
|
|
1750
|
-
|
|
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
|
+
}
|
|
1751
2870
|
return currentCwd;
|
|
1752
2871
|
}
|
|
1753
2872
|
|
|
1754
2873
|
async function refreshSessionForCwdIfNeeded(reason = 'cwd-change') {
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
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;
|
|
1760
2878
|
sessionNeedsCwdRefresh = false;
|
|
1761
|
-
return
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
function buildWorkspaceContext() {
|
|
1765
|
-
try {
|
|
1766
|
-
return formatWorkspaceSessionContext(workspaceRouter.snapshot(currentCwd));
|
|
1767
|
-
} catch (error) {
|
|
1768
|
-
return [
|
|
1769
|
-
'# Workspace',
|
|
1770
|
-
`current cwd: ${currentCwd}`,
|
|
1771
|
-
`project candidates: unavailable (${error?.message || String(error)})`,
|
|
1772
|
-
].join('\n');
|
|
1773
|
-
}
|
|
2879
|
+
return session;
|
|
1774
2880
|
}
|
|
1775
2881
|
|
|
1776
2882
|
function skillContent(name) {
|
|
@@ -1781,6 +2887,12 @@ export async function createMixdogSessionRuntime({
|
|
|
1781
2887
|
return { name, content };
|
|
1782
2888
|
}
|
|
1783
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
|
+
|
|
1784
2896
|
function addProjectSkill(input = {}) {
|
|
1785
2897
|
const name = clean(input.name).replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1786
2898
|
if (!name) throw new Error('skill name is required');
|
|
@@ -1893,30 +3005,25 @@ export async function createMixdogSessionRuntime({
|
|
|
1893
3005
|
return { name, config: { command, args, cwd: resolvedCwd } };
|
|
1894
3006
|
}
|
|
1895
3007
|
|
|
1896
|
-
const
|
|
1897
|
-
|
|
1898
|
-
})();
|
|
1899
|
-
const bridgeStartedAt = performance.now();
|
|
1900
|
-
const bridge = createStandaloneBridge({
|
|
3008
|
+
const agentToolStartedAt = performance.now();
|
|
3009
|
+
const agentTool = createStandaloneAgent({
|
|
1901
3010
|
cfgMod,
|
|
1902
3011
|
reg,
|
|
1903
3012
|
mgr,
|
|
1904
3013
|
dataDir: cfgMod.getPluginData(),
|
|
1905
3014
|
cwd,
|
|
1906
|
-
defaultMode: persistedBridgeMode ?? 'async',
|
|
1907
3015
|
});
|
|
1908
|
-
bootProfile('
|
|
1909
|
-
const
|
|
3016
|
+
bootProfile('agent:ready', { ms: (performance.now() - agentToolStartedAt).toFixed(1) });
|
|
3017
|
+
const agentStatusState = () => {
|
|
1910
3018
|
try {
|
|
1911
|
-
const status =
|
|
3019
|
+
const status = agentTool.getStatus?.({ clientHostPid: session?.clientHostPid || process.pid }) || {};
|
|
1912
3020
|
return {
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
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,
|
|
1917
3024
|
};
|
|
1918
3025
|
} catch {
|
|
1919
|
-
return {
|
|
3026
|
+
return { agentWorkers: [], agentJobs: [], agentScope: null };
|
|
1920
3027
|
}
|
|
1921
3028
|
};
|
|
1922
3029
|
const channelsStartedAt = performance.now();
|
|
@@ -1925,18 +3032,28 @@ export async function createMixdogSessionRuntime({
|
|
|
1925
3032
|
rootDir: STANDALONE_ROOT,
|
|
1926
3033
|
dataDir: cfgMod.getPluginData(),
|
|
1927
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
|
+
},
|
|
1928
3044
|
});
|
|
1929
3045
|
bootProfile('channels:worker-ready', { ms: (performance.now() - channelsStartedAt).toFixed(1) });
|
|
1930
3046
|
const toolsStartedAt = performance.now();
|
|
1931
3047
|
const standaloneTools = [
|
|
1932
3048
|
TOOL_SEARCH_TOOL,
|
|
3049
|
+
SKILL_TOOL,
|
|
1933
3050
|
CWD_TOOL,
|
|
1934
3051
|
EXPLORE_TOOL,
|
|
1935
3052
|
...(searchToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'search' || tool?.name === 'web_fetch'),
|
|
1936
3053
|
...(memoryToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'recall' || tool?.name === 'memory'),
|
|
1937
3054
|
...(channelToolDefs?.TOOL_DEFS || []).filter((tool) => channels.isChannelTool(tool?.name)),
|
|
1938
3055
|
...(codeGraphToolDefs?.CODE_GRAPH_TOOL_DEFS || []).filter((tool) => tool?.name === 'code_graph'),
|
|
1939
|
-
...
|
|
3056
|
+
...agentTool.tools,
|
|
1940
3057
|
PROVIDER_STATUS_TOOL,
|
|
1941
3058
|
CHANNEL_STATUS_TOOL,
|
|
1942
3059
|
].map(applyStandaloneToolDefaults);
|
|
@@ -1957,7 +3074,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1957
3074
|
: [];
|
|
1958
3075
|
const tools = filterDisallowedTools(previewTools, LEAD_DISALLOWED_TOOLS);
|
|
1959
3076
|
const surface = { tools: Array.isArray(tools) ? tools.slice() : [] };
|
|
1960
|
-
applyDeferredToolSurface(surface, mode, standaloneTools);
|
|
3077
|
+
applyDeferredToolSurface(surface, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
1961
3078
|
return surface;
|
|
1962
3079
|
}
|
|
1963
3080
|
|
|
@@ -1972,7 +3089,11 @@ export async function createMixdogSessionRuntime({
|
|
|
1972
3089
|
const selected = Array.isArray(preSessionToolSurface.deferredSelectedTools)
|
|
1973
3090
|
? preSessionToolSurface.deferredSelectedTools
|
|
1974
3091
|
: [];
|
|
1975
|
-
|
|
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));
|
|
1976
3097
|
}
|
|
1977
3098
|
internalTools.setInternalToolsProvider({
|
|
1978
3099
|
tools: standaloneTools,
|
|
@@ -1988,35 +3109,8 @@ export async function createMixdogSessionRuntime({
|
|
|
1988
3109
|
routingSessionId: callerSessionId,
|
|
1989
3110
|
clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
|
|
1990
3111
|
notifyFn: notifyFnForSession(callerSessionId),
|
|
1991
|
-
|
|
1992
|
-
? async (searchArgs) => {
|
|
1993
|
-
const query = Array.isArray(searchArgs.keywords)
|
|
1994
|
-
? searchArgs.keywords.join('\n')
|
|
1995
|
-
: String(searchArgs.keywords || searchArgs.query || '');
|
|
1996
|
-
const prompt = searchArgs.prompt || [
|
|
1997
|
-
'Perform a concise web research task for Mixdog search.',
|
|
1998
|
-
'',
|
|
1999
|
-
`Query: ${query}`,
|
|
2000
|
-
searchArgs.site ? `Site/domain restriction: ${searchArgs.site}` : null,
|
|
2001
|
-
searchArgs.type ? `Search type: ${searchArgs.type}` : null,
|
|
2002
|
-
searchArgs.locale ? `Locale: ${typeof searchArgs.locale === 'string' ? searchArgs.locale : JSON.stringify(searchArgs.locale)}` : null,
|
|
2003
|
-
`Max results: ${Math.max(1, Math.min(20, Number(searchArgs.maxResults) || 10))}`,
|
|
2004
|
-
'',
|
|
2005
|
-
'Return a short answer first, then cite useful results as title + URL + one-line snippet.',
|
|
2006
|
-
'Do not edit files.',
|
|
2007
|
-
].filter(Boolean).join('\n');
|
|
2008
|
-
const rendered = await bridge.execute({
|
|
2009
|
-
type: 'spawn',
|
|
2010
|
-
agent: 'web-researcher',
|
|
2011
|
-
tag: `search_${Date.now().toString(36)}`,
|
|
2012
|
-
prompt,
|
|
2013
|
-
cwd: callerCwd,
|
|
2014
|
-
wait: true,
|
|
2015
|
-
firstResponseTimeoutMs: Number.isFinite(Number(searchArgs.firstResponseTimeoutMs)) ? Number(searchArgs.firstResponseTimeoutMs) : 120_000,
|
|
2016
|
-
idleTimeoutMs: Number.isFinite(Number(searchArgs.idleTimeoutMs)) ? Number(searchArgs.idleTimeoutMs) : 30 * 60_000,
|
|
2017
|
-
}, { invocationSource: 'user-command', callerCwd });
|
|
2018
|
-
return String(rendered || '').replace(/^bridge result[^\n]*\n/i, '').trim();
|
|
2019
|
-
}
|
|
3112
|
+
nativeSearch: name === 'search'
|
|
3113
|
+
? async (searchArgs) => runNativeWebSearch(searchArgs, { signal: callerCtx?.signal || session?.controller?.signal })
|
|
2020
3114
|
: undefined,
|
|
2021
3115
|
});
|
|
2022
3116
|
}
|
|
@@ -2030,7 +3124,9 @@ export async function createMixdogSessionRuntime({
|
|
|
2030
3124
|
if (!codeGraphMod?.executeCodeGraphTool) throw new Error('code_graph runtime is not available');
|
|
2031
3125
|
return await codeGraphMod.executeCodeGraphTool(name, args || {}, args?.cwd || callerCwd);
|
|
2032
3126
|
}
|
|
2033
|
-
if (name === 'tool_search')
|
|
3127
|
+
if (name === 'tool_search') {
|
|
3128
|
+
return renderToolSearch(args, activeToolSurface(), mode);
|
|
3129
|
+
}
|
|
2034
3130
|
if (name === 'explore') {
|
|
2035
3131
|
const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
|
|
2036
3132
|
return await runExplore(args || {}, {
|
|
@@ -2050,9 +3146,12 @@ export async function createMixdogSessionRuntime({
|
|
|
2050
3146
|
}
|
|
2051
3147
|
return JSON.stringify({ cwd: currentCwd, sessionId: session?.id || null }, null, 2);
|
|
2052
3148
|
}
|
|
2053
|
-
if (name === '
|
|
3149
|
+
if (name === 'Skill') {
|
|
3150
|
+
return skillToolContent(args?.name);
|
|
3151
|
+
}
|
|
3152
|
+
if (name === 'agent') {
|
|
2054
3153
|
const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
|
|
2055
|
-
return await
|
|
3154
|
+
return await agentTool.execute(args, {
|
|
2056
3155
|
callerCwd,
|
|
2057
3156
|
invocationSource: 'model-tool',
|
|
2058
3157
|
callerSessionId,
|
|
@@ -2061,9 +3160,12 @@ export async function createMixdogSessionRuntime({
|
|
|
2061
3160
|
notifyFn: notifyFnForSession(callerSessionId),
|
|
2062
3161
|
});
|
|
2063
3162
|
}
|
|
2064
|
-
if (name === 'provider_status') return renderProviderStatus(
|
|
3163
|
+
if (name === 'provider_status') return renderProviderStatus(displayConfig());
|
|
2065
3164
|
if (name === 'channel_status') return renderChannelStatus();
|
|
2066
|
-
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
|
+
}
|
|
2067
3169
|
throw new Error(`unknown standalone internal tool: ${name}`);
|
|
2068
3170
|
},
|
|
2069
3171
|
});
|
|
@@ -2082,16 +3184,81 @@ export async function createMixdogSessionRuntime({
|
|
|
2082
3184
|
function invalidateProviderCaches() {
|
|
2083
3185
|
providerModelsCache = { models: null, at: 0 };
|
|
2084
3186
|
providerModelsPromise = null;
|
|
3187
|
+
searchProviderModelsCache = { models: null, at: 0 };
|
|
3188
|
+
searchProviderModelsPromise = null;
|
|
3189
|
+
usageDashboardCache = { dashboard: null, at: 0 };
|
|
3190
|
+
usageDashboardPromise = null;
|
|
2085
3191
|
providerSetupCache = { setup: null, at: 0 };
|
|
3192
|
+
providerSetupQuickCache = { setup: null, at: 0 };
|
|
2086
3193
|
providerSetupPromise = null;
|
|
2087
3194
|
providerInitPromise = null;
|
|
2088
3195
|
modelMetaByRoute.clear();
|
|
2089
3196
|
}
|
|
2090
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
|
+
|
|
2091
3256
|
function ensureFullConfig() {
|
|
2092
3257
|
if (configHasSecrets) return config;
|
|
2093
|
-
|
|
2094
|
-
|
|
3258
|
+
return reloadFullConfig();
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
function displayConfig() {
|
|
2095
3262
|
return config;
|
|
2096
3263
|
}
|
|
2097
3264
|
|
|
@@ -2104,6 +3271,19 @@ export async function createMixdogSessionRuntime({
|
|
|
2104
3271
|
return ensureFullConfig();
|
|
2105
3272
|
}
|
|
2106
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
|
+
|
|
2107
3287
|
async function ensureProvidersReady(providerConfig = config.providers || {}) {
|
|
2108
3288
|
if (providerInitPromise) return await providerInitPromise;
|
|
2109
3289
|
providerInitPromise = reg.initProviders(providerConfig)
|
|
@@ -2113,13 +3293,23 @@ export async function createMixdogSessionRuntime({
|
|
|
2113
3293
|
return await providerInitPromise;
|
|
2114
3294
|
}
|
|
2115
3295
|
|
|
2116
|
-
async function cachedProviderSetup({ force = false } = {}) {
|
|
2117
|
-
|
|
2118
|
-
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) {
|
|
2119
3298
|
return providerSetupCache.setup;
|
|
2120
3299
|
}
|
|
2121
|
-
if (
|
|
2122
|
-
|
|
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 })
|
|
2123
3313
|
.then((setup) => {
|
|
2124
3314
|
providerSetupCache = { setup, at: Date.now() };
|
|
2125
3315
|
return setup;
|
|
@@ -2267,9 +3457,10 @@ function parsedProviderModelVersion(id) {
|
|
|
2267
3457
|
description: m.description || '',
|
|
2268
3458
|
supportsVision: m.supportsVision === true,
|
|
2269
3459
|
supportsFunctionCalling: m.supportsFunctionCalling === true,
|
|
3460
|
+
supportsWebSearch: searchCapableFor(name, m),
|
|
2270
3461
|
supportsPromptCaching: m.supportsPromptCaching === true,
|
|
2271
3462
|
supportsReasoning: m.supportsReasoning === true,
|
|
2272
|
-
reasoningLevels: Array.isArray(m.reasoningLevels) ? m.reasoningLevels :
|
|
3463
|
+
reasoningLevels: Array.isArray(m.reasoningLevels) ? m.reasoningLevels : undefined,
|
|
2273
3464
|
reasoningOptions: Array.isArray(m.reasoningOptions) ? m.reasoningOptions : [],
|
|
2274
3465
|
reasoningContentField: m.reasoningContentField || null,
|
|
2275
3466
|
mode: m.mode || null,
|
|
@@ -2291,40 +3482,265 @@ function parsedProviderModelVersion(id) {
|
|
|
2291
3482
|
return sortProviderModels((rows || []).map(hydrateProviderModelRow));
|
|
2292
3483
|
}
|
|
2293
3484
|
|
|
2294
|
-
|
|
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() {
|
|
2295
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 [];
|
|
2296
3663
|
await ensureProvidersReady(config.providers || {});
|
|
2297
|
-
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
|
+
}));
|
|
2298
3690
|
const results = [];
|
|
2299
3691
|
const seen = new Set();
|
|
2300
|
-
|
|
2301
|
-
|
|
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 [];
|
|
2302
3708
|
try {
|
|
2303
3709
|
const models = await provider.listModels();
|
|
2304
|
-
if (Array.isArray(models))
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
seen.add(key);
|
|
2311
|
-
const row = providerModelCacheRow(name, m);
|
|
2312
|
-
results.push(row);
|
|
2313
|
-
modelMetaByRoute.set(modelMetaKey(name, m.id), row);
|
|
2314
|
-
}
|
|
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));
|
|
2315
3716
|
}
|
|
3717
|
+
return rows;
|
|
2316
3718
|
} catch {
|
|
2317
3719
|
// Ignore per-provider catalog failures so one bad credential or
|
|
2318
3720
|
// transient /models error does not hide other authenticated models.
|
|
3721
|
+
return [];
|
|
2319
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);
|
|
2320
3732
|
}
|
|
2321
3733
|
return results;
|
|
2322
3734
|
}
|
|
2323
3735
|
|
|
2324
|
-
async function collectProviderModels({ force = false } = {}) {
|
|
3736
|
+
async function collectProviderModels({ force = false, quick = false } = {}) {
|
|
2325
3737
|
if (!force && Array.isArray(providerModelsCache.models)) {
|
|
2326
3738
|
return providerModelsFromCacheRows(providerModelsCache.models);
|
|
2327
3739
|
}
|
|
3740
|
+
if (!force && quick) {
|
|
3741
|
+
warmProviderModelCache();
|
|
3742
|
+
return quickProviderModelRows();
|
|
3743
|
+
}
|
|
2328
3744
|
if (!providerModelsPromise) {
|
|
2329
3745
|
providerModelsPromise = loadProviderModelsFresh()
|
|
2330
3746
|
.then((models) => {
|
|
@@ -2362,7 +3778,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2362
3778
|
const models = await collectProviderModels();
|
|
2363
3779
|
const picked = models[0] || null;
|
|
2364
3780
|
if (!picked) {
|
|
2365
|
-
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.');
|
|
2366
3782
|
}
|
|
2367
3783
|
route = {
|
|
2368
3784
|
...route,
|
|
@@ -2395,7 +3811,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2395
3811
|
|
|
2396
3812
|
function requireModelRoute() {
|
|
2397
3813
|
if (routeHasModel()) return;
|
|
2398
|
-
throw new Error('No model configured.
|
|
3814
|
+
throw new Error('No model configured. Open /providers to sign in, then /model to choose a model.');
|
|
2399
3815
|
}
|
|
2400
3816
|
|
|
2401
3817
|
async function recreateCurrentSessionIfReady() {
|
|
@@ -2434,8 +3850,8 @@ function parsedProviderModelVersion(id) {
|
|
|
2434
3850
|
const coreMemoryContext = await loadCoreMemoryContext();
|
|
2435
3851
|
if (closeRequested) throw new Error('runtime is closing');
|
|
2436
3852
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
3853
|
+
const workflow = activeWorkflowSummary(config, dataDir);
|
|
2437
3854
|
const workflowContext = workflowContextBlock(config, dataDir);
|
|
2438
|
-
const workspaceContext = buildWorkspaceContext();
|
|
2439
3855
|
const sessionOpts = {
|
|
2440
3856
|
provider: route.provider,
|
|
2441
3857
|
model: route.model,
|
|
@@ -2450,9 +3866,12 @@ function parsedProviderModelVersion(id) {
|
|
|
2450
3866
|
disallowedTools: LEAD_DISALLOWED_TOOLS,
|
|
2451
3867
|
cwd: currentCwd,
|
|
2452
3868
|
coreMemoryContext,
|
|
3869
|
+
workflow,
|
|
2453
3870
|
workflowContext,
|
|
2454
|
-
workspaceContext,
|
|
2455
3871
|
fast: route.fast === true,
|
|
3872
|
+
compaction: config.compaction && typeof config.compaction === 'object'
|
|
3873
|
+
? normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() })
|
|
3874
|
+
: undefined,
|
|
2456
3875
|
};
|
|
2457
3876
|
if (hasOwn(route, 'effort') || route.effectiveEffort) {
|
|
2458
3877
|
sessionOpts.effort = route.effectiveEffort || null;
|
|
@@ -2460,15 +3879,55 @@ function parsedProviderModelVersion(id) {
|
|
|
2460
3879
|
session = mgr.createSession(sessionOpts);
|
|
2461
3880
|
sessionNeedsCwdRefresh = false;
|
|
2462
3881
|
Object.defineProperty(session, 'beforeToolHook', {
|
|
2463
|
-
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
|
+
})),
|
|
2464
3906
|
enumerable: false,
|
|
2465
3907
|
configurable: true,
|
|
2466
3908
|
writable: true,
|
|
2467
3909
|
});
|
|
2468
|
-
applyDeferredToolSurface(session, mode, standaloneTools);
|
|
3910
|
+
applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
2469
3911
|
applyPreSessionToolSelection();
|
|
2470
|
-
statusRoutes
|
|
3912
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
2471
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 */ }
|
|
2472
3931
|
bootProfile('session:create:ready', {
|
|
2473
3932
|
ms: (performance.now() - startedAt).toFixed(1),
|
|
2474
3933
|
reason,
|
|
@@ -2487,7 +3946,10 @@ function parsedProviderModelVersion(id) {
|
|
|
2487
3946
|
}
|
|
2488
3947
|
|
|
2489
3948
|
function scheduleLeadSessionPrewarm() {
|
|
2490
|
-
if (
|
|
3949
|
+
if (!sessionPrewarmEnabled) {
|
|
3950
|
+
bootProfile('session:prewarm-skipped');
|
|
3951
|
+
return;
|
|
3952
|
+
}
|
|
2491
3953
|
const timer = setTimeout(() => {
|
|
2492
3954
|
if (closeRequested || session?.id || sessionCreatePromise || activeTurnCount > 0) return;
|
|
2493
3955
|
void createCurrentSession('prewarm')
|
|
@@ -2498,7 +3960,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2498
3960
|
}
|
|
2499
3961
|
|
|
2500
3962
|
function scheduleProviderWarmup(delayMs = providerWarmupDelayMs) {
|
|
2501
|
-
if (
|
|
3963
|
+
if (!providerWarmupEnabled) {
|
|
2502
3964
|
bootProfile('providers:warm-skipped');
|
|
2503
3965
|
return;
|
|
2504
3966
|
}
|
|
@@ -2506,6 +3968,10 @@ function parsedProviderModelVersion(id) {
|
|
|
2506
3968
|
providerWarmupTimer = setTimeout(() => {
|
|
2507
3969
|
providerWarmupTimer = null;
|
|
2508
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
|
+
}
|
|
2509
3975
|
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
2510
3976
|
bootProfile('providers:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
2511
3977
|
scheduleProviderWarmup(backgroundBusyRetryMs);
|
|
@@ -2513,8 +3979,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2513
3979
|
}
|
|
2514
3980
|
const providersStartedAt = performance.now();
|
|
2515
3981
|
try {
|
|
2516
|
-
|
|
2517
|
-
configHasSecrets = true;
|
|
3982
|
+
reloadFullConfig();
|
|
2518
3983
|
} catch (error) {
|
|
2519
3984
|
bootProfile('config:full-failed', { error: error?.message || String(error) });
|
|
2520
3985
|
}
|
|
@@ -2522,28 +3987,27 @@ function parsedProviderModelVersion(id) {
|
|
|
2522
3987
|
.then(() => {
|
|
2523
3988
|
bootProfile('providers:init:ready', { ms: (performance.now() - providersStartedAt).toFixed(1) });
|
|
2524
3989
|
if (closeRequested) return null;
|
|
2525
|
-
|
|
2526
|
-
bootProfile('providers:optional-warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
2527
|
-
scheduleProviderWarmup(backgroundBusyRetryMs);
|
|
2528
|
-
return null;
|
|
2529
|
-
}
|
|
2530
|
-
return cachedProviderSetup();
|
|
2531
|
-
})
|
|
2532
|
-
.then((setup) => {
|
|
2533
|
-
if (!setup) return;
|
|
2534
|
-
bootProfile('provider-setup:warm-ready', {
|
|
2535
|
-
api: Array.isArray(setup?.api) ? setup.api.length : 0,
|
|
2536
|
-
oauth: Array.isArray(setup?.oauth) ? setup.oauth.length : 0,
|
|
2537
|
-
local: Array.isArray(setup?.local) ? setup.local.length : 0,
|
|
2538
|
-
});
|
|
3990
|
+
return true;
|
|
2539
3991
|
})
|
|
2540
3992
|
.catch((error) => bootProfile('providers:warm-failed', { error: error?.message || String(error) }));
|
|
2541
3993
|
}, delayMs);
|
|
2542
3994
|
providerWarmupTimer.unref?.();
|
|
2543
3995
|
}
|
|
2544
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
|
+
|
|
2545
4009
|
function scheduleProviderModelWarmup(delayMs = providerModelWarmupDelayMs) {
|
|
2546
|
-
if (
|
|
4010
|
+
if (!modelPrefetchEnabled) return;
|
|
2547
4011
|
if (providerModelWarmupTimer || closeRequested) return;
|
|
2548
4012
|
providerModelWarmupTimer = setTimeout(() => {
|
|
2549
4013
|
providerModelWarmupTimer = null;
|
|
@@ -2558,11 +4022,75 @@ function parsedProviderModelVersion(id) {
|
|
|
2558
4022
|
providerModelWarmupTimer.unref?.();
|
|
2559
4023
|
}
|
|
2560
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
|
+
|
|
2561
4085
|
function scheduleChannelStart(delayMs = channelStartDelayMs) {
|
|
2562
4086
|
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
2563
4087
|
bootProfile('channels:start-skipped');
|
|
2564
4088
|
return;
|
|
2565
4089
|
}
|
|
4090
|
+
if (!channelsEnabled()) {
|
|
4091
|
+
bootProfile('channels:start-disabled');
|
|
4092
|
+
return;
|
|
4093
|
+
}
|
|
2566
4094
|
if (channelStartTimer || closeRequested) return;
|
|
2567
4095
|
bootProfile('channels:start-scheduled', { delayMs });
|
|
2568
4096
|
channelStartTimer = setTimeout(() => {
|
|
@@ -2597,12 +4125,31 @@ function parsedProviderModelVersion(id) {
|
|
|
2597
4125
|
});
|
|
2598
4126
|
}
|
|
2599
4127
|
|
|
2600
|
-
bootProfile('session-runtime:ready', {
|
|
4128
|
+
bootProfile('session-runtime:ready', {
|
|
4129
|
+
lazySession: true,
|
|
4130
|
+
prewarmSession: sessionPrewarmEnabled,
|
|
4131
|
+
providerWarmup: providerWarmupEnabled,
|
|
4132
|
+
codeGraphPrewarm: codeGraphPrewarmEnabled,
|
|
4133
|
+
});
|
|
2601
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();
|
|
2602
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();
|
|
2603
4150
|
scheduleChannelStart();
|
|
2604
4151
|
|
|
2605
|
-
function contextStatusCacheKeyFor({ messages, tools
|
|
4152
|
+
function contextStatusCacheKeyFor({ messages, tools }) {
|
|
2606
4153
|
const compaction = session?.compaction || {};
|
|
2607
4154
|
const lastMessage = messages[messages.length - 1] || null;
|
|
2608
4155
|
return {
|
|
@@ -2612,7 +4159,6 @@ function parsedProviderModelVersion(id) {
|
|
|
2612
4159
|
model: route.model,
|
|
2613
4160
|
cwd: currentCwd,
|
|
2614
4161
|
mode,
|
|
2615
|
-
bridgeMode,
|
|
2616
4162
|
messages,
|
|
2617
4163
|
messageCount: messages.length,
|
|
2618
4164
|
lastMessage,
|
|
@@ -2685,23 +4231,25 @@ function parsedProviderModelVersion(id) {
|
|
|
2685
4231
|
get toolMode() {
|
|
2686
4232
|
return mode;
|
|
2687
4233
|
},
|
|
2688
|
-
get bridgeMode() {
|
|
2689
|
-
return bridge.getDefaultMode();
|
|
2690
|
-
},
|
|
2691
4234
|
get autoClear() {
|
|
2692
4235
|
return normalizeAutoClearConfig(config.autoClear);
|
|
2693
4236
|
},
|
|
2694
4237
|
get systemShell() {
|
|
2695
4238
|
return normalizeSystemShellConfig(config.shell);
|
|
2696
4239
|
},
|
|
4240
|
+
get searchRoute() {
|
|
4241
|
+
searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
4242
|
+
return searchRoute;
|
|
4243
|
+
},
|
|
2697
4244
|
get workflow() {
|
|
2698
4245
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
2699
|
-
|
|
2700
|
-
|
|
4246
|
+
if (session?.workflow && typeof session.workflow === 'object') {
|
|
4247
|
+
return workflowSummary(session.workflow);
|
|
4248
|
+
}
|
|
4249
|
+
return activeWorkflowSummary(config, dataDir);
|
|
2701
4250
|
},
|
|
2702
4251
|
get outputStyle() {
|
|
2703
|
-
|
|
2704
|
-
return outputStyleStatus(dataDir).current;
|
|
4252
|
+
return getOutputStyleStatusCached().current;
|
|
2705
4253
|
},
|
|
2706
4254
|
get cwd() {
|
|
2707
4255
|
return currentCwd;
|
|
@@ -2710,22 +4258,31 @@ function parsedProviderModelVersion(id) {
|
|
|
2710
4258
|
return session;
|
|
2711
4259
|
},
|
|
2712
4260
|
contextStatus() {
|
|
2713
|
-
|
|
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 : []);
|
|
2714
4269
|
const tools = Array.isArray(session?.tools) ? session.tools : [];
|
|
2715
|
-
const
|
|
2716
|
-
const cacheKey = contextStatusCacheKeyFor({ messages, tools, bridgeMode });
|
|
4270
|
+
const cacheKey = contextStatusCacheKeyFor({ messages, tools });
|
|
2717
4271
|
if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
|
|
2718
4272
|
return contextStatusCacheValue;
|
|
2719
4273
|
}
|
|
2720
4274
|
|
|
2721
4275
|
const messageSummary = summarizeContextMessages(messages);
|
|
2722
4276
|
const toolSchemaTokens = estimateToolSchemaTokens(tools);
|
|
4277
|
+
const toolSchemaBreakdown = estimateToolSchemaBreakdown(tools);
|
|
2723
4278
|
const requestReserveTokens = estimateRequestReserveTokens(tools);
|
|
2724
4279
|
const requestOverheadTokens = Math.max(0, requestReserveTokens - toolSchemaTokens);
|
|
2725
4280
|
const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
|
|
2726
4281
|
const effectiveWindow = Number(session?.contextWindow || rawWindow || 0);
|
|
2727
4282
|
const lastContextTokens = Number(session?.lastContextTokens || 0);
|
|
2728
|
-
const estimatedContextTokens =
|
|
4283
|
+
const estimatedContextTokens = estimateTranscriptContextUsage(messages, tools, {
|
|
4284
|
+
messageCount: messageSummary.count,
|
|
4285
|
+
});
|
|
2729
4286
|
const compactAt = Number(session?.compaction?.lastChangedAt || session?.compaction?.lastCompactAt || 0);
|
|
2730
4287
|
const usageAt = Number(session?.lastContextTokensUpdatedAt || 0);
|
|
2731
4288
|
const lastUsageStale = !!lastContextTokens && (
|
|
@@ -2735,31 +4292,58 @@ function parsedProviderModelVersion(id) {
|
|
|
2735
4292
|
);
|
|
2736
4293
|
const compactBoundaryTokens = Number(session?.compactBoundaryTokens || session?.compaction?.boundaryTokens || 0);
|
|
2737
4294
|
const displayWindow = compactBoundaryTokens || effectiveWindow;
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
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;
|
|
2741
4302
|
const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
|
|
2742
4303
|
const autoCompactTokenLimit = Number(session?.autoCompactTokenLimit || 0);
|
|
2743
|
-
|
|
2744
|
-
|
|
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
|
|
2745
4330
|
? autoCompactTokenLimit
|
|
2746
|
-
:
|
|
2747
|
-
const compactBufferTokens = Number(
|
|
4331
|
+
: (usablePersistedTrigger || defaultCompactTriggerTokens || 0);
|
|
4332
|
+
const compactBufferTokens = Number(compactBoundaryTokens && compactTriggerTokens
|
|
4333
|
+
? Math.max(0, compactBoundaryTokens - compactTriggerTokens)
|
|
4334
|
+
: defaultCompactBufferTokens);
|
|
2748
4335
|
const value = {
|
|
2749
4336
|
sessionId: session?.id || null,
|
|
2750
4337
|
provider: route.provider,
|
|
2751
4338
|
model: route.model,
|
|
2752
4339
|
cwd: currentCwd,
|
|
2753
4340
|
toolMode: mode,
|
|
2754
|
-
bridgeMode,
|
|
2755
4341
|
contextWindow: displayWindow || effectiveWindow || null,
|
|
2756
4342
|
effectiveContextWindow: effectiveWindow || null,
|
|
2757
4343
|
rawContextWindow: rawWindow || null,
|
|
2758
4344
|
effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
|
|
2759
4345
|
usedTokens,
|
|
2760
|
-
usedSource:
|
|
2761
|
-
? 'last_api_request'
|
|
2762
|
-
: 'estimated',
|
|
4346
|
+
usedSource: 'estimated',
|
|
2763
4347
|
currentEstimatedTokens: estimatedContextTokens,
|
|
2764
4348
|
lastApiRequestTokens: lastContextTokens || 0,
|
|
2765
4349
|
lastApiRequestStale: lastUsageStale,
|
|
@@ -2776,6 +4360,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2776
4360
|
messages: messageSummary,
|
|
2777
4361
|
request: {
|
|
2778
4362
|
toolSchemaTokens,
|
|
4363
|
+
toolSchemaBreakdown,
|
|
2779
4364
|
requestOverheadTokens,
|
|
2780
4365
|
reserveTokens: requestReserveTokens,
|
|
2781
4366
|
},
|
|
@@ -2796,26 +4381,64 @@ function parsedProviderModelVersion(id) {
|
|
|
2796
4381
|
return value;
|
|
2797
4382
|
},
|
|
2798
4383
|
listProviders() {
|
|
2799
|
-
return renderProviderStatus(
|
|
4384
|
+
return renderProviderStatus(displayConfig());
|
|
2800
4385
|
},
|
|
2801
4386
|
async getProviderSetup() {
|
|
2802
4387
|
return await cachedProviderSetup();
|
|
2803
4388
|
},
|
|
2804
4389
|
async getUsageDashboard(options = {}) {
|
|
2805
|
-
const
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
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;
|
|
2816
4439
|
},
|
|
2817
4440
|
getOnboardingStatus() {
|
|
2818
|
-
const nextConfig =
|
|
4441
|
+
const nextConfig = displayConfig();
|
|
2819
4442
|
return {
|
|
2820
4443
|
completed: nextConfig?.onboarding?.completed === true,
|
|
2821
4444
|
version: nextConfig?.onboarding?.version || 0,
|
|
@@ -2826,17 +4449,118 @@ function parsedProviderModelVersion(id) {
|
|
|
2826
4449
|
getAutoClear() {
|
|
2827
4450
|
return normalizeAutoClearConfig(config.autoClear);
|
|
2828
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
|
+
},
|
|
2829
4555
|
getSystemShell() {
|
|
2830
4556
|
return normalizeSystemShellConfig(config.shell);
|
|
2831
4557
|
},
|
|
2832
4558
|
setSystemShell(input = {}) {
|
|
2833
4559
|
const command = normalizeSystemShellCommand(typeof input === 'string' ? input : input?.command);
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
...
|
|
2837
|
-
shell: command ? { ...(nextConfig.shell || {}), command } : {},
|
|
4560
|
+
saveConfigAndAdopt({
|
|
4561
|
+
...config,
|
|
4562
|
+
shell: command ? { ...(config.shell || {}), command } : {},
|
|
2838
4563
|
});
|
|
2839
|
-
config = cfgMod.loadConfig();
|
|
2840
4564
|
setConfiguredShell(command);
|
|
2841
4565
|
return normalizeSystemShellConfig(config.shell);
|
|
2842
4566
|
},
|
|
@@ -2855,9 +4579,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2855
4579
|
next.idleMs = idleMs;
|
|
2856
4580
|
if (!hasOwn(input, 'enabled')) next.enabled = true;
|
|
2857
4581
|
}
|
|
2858
|
-
|
|
2859
|
-
cfgMod.saveConfig({ ...nextConfig, autoClear: next });
|
|
2860
|
-
config = cfgMod.loadConfig();
|
|
4582
|
+
saveConfigAndAdopt({ ...config, autoClear: next });
|
|
2861
4583
|
return { ...normalizeAutoClearConfig(config.autoClear), label: formatDurationMs(normalizeAutoClearConfig(config.autoClear).idleMs) };
|
|
2862
4584
|
},
|
|
2863
4585
|
async completeOnboarding(payload = {}) {
|
|
@@ -2865,7 +4587,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2865
4587
|
const workflowInput = payload.workflowRoutes && typeof payload.workflowRoutes === 'object'
|
|
2866
4588
|
? payload.workflowRoutes
|
|
2867
4589
|
: {};
|
|
2868
|
-
const nextConfig =
|
|
4590
|
+
const nextConfig = { ...config };
|
|
2869
4591
|
let presets = Array.isArray(nextConfig.presets) ? nextConfig.presets.slice() : [];
|
|
2870
4592
|
const workflowRoutes = { ...(nextConfig.workflowRoutes || {}) };
|
|
2871
4593
|
|
|
@@ -2884,10 +4606,13 @@ function parsedProviderModelVersion(id) {
|
|
|
2884
4606
|
|
|
2885
4607
|
nextConfig.presets = presets;
|
|
2886
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.
|
|
2887
4612
|
nextConfig.maintenance = {
|
|
2888
4613
|
...(nextConfig.maintenance || {}),
|
|
2889
|
-
|
|
2890
|
-
|
|
4614
|
+
...(workflowRoutes.explorer ? { explore: normalizeWorkflowRoute(workflowRoutes.explorer) } : {}),
|
|
4615
|
+
...(workflowRoutes.memory ? { memory: normalizeWorkflowRoute(workflowRoutes.memory) } : {}),
|
|
2891
4616
|
};
|
|
2892
4617
|
nextConfig.onboarding = {
|
|
2893
4618
|
...(nextConfig.onboarding || {}),
|
|
@@ -2896,8 +4621,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2896
4621
|
completedAt: new Date().toISOString(),
|
|
2897
4622
|
};
|
|
2898
4623
|
|
|
2899
|
-
|
|
2900
|
-
config = cfgMod.loadConfig();
|
|
4624
|
+
saveConfigAndAdopt(nextConfig);
|
|
2901
4625
|
if (defaultRoute) {
|
|
2902
4626
|
route = resolveRoute(config, { provider: defaultRoute.provider, model: defaultRoute.model, effort: defaultRoute.effort });
|
|
2903
4627
|
if (session?.id) mgr.closeSession(session.id, 'cli-onboarding-complete');
|
|
@@ -2980,26 +4704,34 @@ function parsedProviderModelVersion(id) {
|
|
|
2980
4704
|
const result = String(secret || '').trim()
|
|
2981
4705
|
? saveProviderApiKey(cfgMod, providerId, secret)
|
|
2982
4706
|
: await loginOAuthProvider(cfgMod, providerId);
|
|
2983
|
-
|
|
4707
|
+
reloadFullConfig();
|
|
2984
4708
|
invalidateProviderCaches();
|
|
2985
4709
|
warmProviderModelCache();
|
|
2986
4710
|
return result;
|
|
2987
4711
|
},
|
|
2988
4712
|
async loginOAuthProvider(providerId) {
|
|
2989
4713
|
const result = await loginOAuthProvider(cfgMod, providerId);
|
|
2990
|
-
|
|
4714
|
+
reloadFullConfig();
|
|
2991
4715
|
invalidateProviderCaches();
|
|
2992
4716
|
warmProviderModelCache();
|
|
2993
4717
|
return result;
|
|
2994
4718
|
},
|
|
2995
4719
|
async beginOAuthProviderLogin(providerId) {
|
|
2996
4720
|
const result = await beginOAuthProviderLogin(cfgMod, providerId);
|
|
2997
|
-
|
|
4721
|
+
reloadFullConfig();
|
|
2998
4722
|
return {
|
|
2999
4723
|
...result,
|
|
4724
|
+
waitForCallback: result.waitForCallback?.then((completed) => {
|
|
4725
|
+
reloadFullConfig();
|
|
4726
|
+
if (completed) {
|
|
4727
|
+
invalidateProviderCaches();
|
|
4728
|
+
warmProviderModelCache();
|
|
4729
|
+
}
|
|
4730
|
+
return completed;
|
|
4731
|
+
}),
|
|
3000
4732
|
completeCode: async (code) => {
|
|
3001
4733
|
const completed = await result.completeCode(code);
|
|
3002
|
-
|
|
4734
|
+
reloadFullConfig();
|
|
3003
4735
|
invalidateProviderCaches();
|
|
3004
4736
|
warmProviderModelCache();
|
|
3005
4737
|
return completed;
|
|
@@ -3008,42 +4740,97 @@ function parsedProviderModelVersion(id) {
|
|
|
3008
4740
|
},
|
|
3009
4741
|
saveProviderApiKey(providerId, secret) {
|
|
3010
4742
|
const result = saveProviderApiKey(cfgMod, providerId, secret);
|
|
3011
|
-
|
|
4743
|
+
reloadFullConfig();
|
|
3012
4744
|
invalidateProviderCaches();
|
|
3013
4745
|
warmProviderModelCache();
|
|
3014
4746
|
return result;
|
|
3015
4747
|
},
|
|
3016
4748
|
saveOpenAIUsageSessionKey(secret) {
|
|
3017
4749
|
const result = saveOpenAIUsageSessionKey(cfgMod, secret);
|
|
3018
|
-
|
|
4750
|
+
reloadFullConfig();
|
|
3019
4751
|
invalidateProviderCaches();
|
|
3020
4752
|
return result;
|
|
3021
4753
|
},
|
|
3022
4754
|
saveOpenCodeGoUsageAuth(opts) {
|
|
3023
4755
|
const result = saveOpenCodeGoUsageAuth(cfgMod, opts);
|
|
3024
|
-
|
|
4756
|
+
reloadFullConfig();
|
|
3025
4757
|
invalidateProviderCaches();
|
|
3026
4758
|
return result;
|
|
3027
4759
|
},
|
|
3028
4760
|
setLocalProvider(providerId, opts) {
|
|
3029
4761
|
const result = setLocalProvider(cfgMod, providerId, opts);
|
|
3030
|
-
|
|
4762
|
+
reloadFullConfig();
|
|
3031
4763
|
invalidateProviderCaches();
|
|
3032
4764
|
warmProviderModelCache();
|
|
3033
4765
|
return result;
|
|
3034
4766
|
},
|
|
3035
4767
|
forgetProviderAuth(providerId) {
|
|
3036
|
-
const result = forgetProviderAuth(providerId);
|
|
3037
|
-
|
|
4768
|
+
const result = forgetProviderAuth(cfgMod, providerId);
|
|
4769
|
+
reloadFullConfig();
|
|
3038
4770
|
invalidateProviderCaches();
|
|
3039
4771
|
warmProviderModelCache();
|
|
3040
4772
|
return result;
|
|
3041
4773
|
},
|
|
3042
4774
|
listPresets() {
|
|
3043
|
-
return cfgMod.listPresets(
|
|
4775
|
+
return cfgMod.listPresets(displayConfig());
|
|
3044
4776
|
},
|
|
3045
4777
|
async listProviderModels(options = {}) {
|
|
3046
|
-
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;
|
|
3047
4834
|
},
|
|
3048
4835
|
listAgents() {
|
|
3049
4836
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
@@ -3055,7 +4842,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3055
4842
|
}));
|
|
3056
4843
|
},
|
|
3057
4844
|
listWorkflows() {
|
|
3058
|
-
const currentConfig =
|
|
4845
|
+
const currentConfig = displayConfig();
|
|
3059
4846
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
3060
4847
|
const active = activeWorkflowId(currentConfig);
|
|
3061
4848
|
return listWorkflowPacks(dataDir).map((workflow) => ({
|
|
@@ -3068,16 +4855,13 @@ function parsedProviderModelVersion(id) {
|
|
|
3068
4855
|
}));
|
|
3069
4856
|
},
|
|
3070
4857
|
getOutputStyle() {
|
|
3071
|
-
|
|
3072
|
-
return outputStyleStatus(dataDir);
|
|
4858
|
+
return getOutputStyleStatusCached();
|
|
3073
4859
|
},
|
|
3074
4860
|
listOutputStyles() {
|
|
3075
|
-
|
|
3076
|
-
return outputStyleStatus(dataDir);
|
|
4861
|
+
return getOutputStyleStatusCached();
|
|
3077
4862
|
},
|
|
3078
4863
|
async setOutputStyle(value) {
|
|
3079
|
-
const
|
|
3080
|
-
const before = outputStyleStatus(dataDir);
|
|
4864
|
+
const before = getOutputStyleStatusCached({ fresh: true });
|
|
3081
4865
|
const selected = findOutputStyle(value, before.styles);
|
|
3082
4866
|
if (!selected) {
|
|
3083
4867
|
const names = before.styles.map((style) => style.label || style.id).join(', ') || 'Default';
|
|
@@ -3093,6 +4877,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3093
4877
|
}
|
|
3094
4878
|
return next;
|
|
3095
4879
|
});
|
|
4880
|
+
invalidateOutputStyleStatusCache();
|
|
3096
4881
|
const hasConversation = sessionHasConversationMessages(session);
|
|
3097
4882
|
let appliedToCurrentSession = !hasConversation;
|
|
3098
4883
|
if (session?.id && !hasConversation) {
|
|
@@ -3101,23 +4886,17 @@ function parsedProviderModelVersion(id) {
|
|
|
3101
4886
|
await recreateCurrentSessionIfReady();
|
|
3102
4887
|
}
|
|
3103
4888
|
invalidateContextStatusCache();
|
|
3104
|
-
return { ...
|
|
4889
|
+
return { ...getOutputStyleStatusCached({ fresh: true }), appliedToCurrentSession };
|
|
3105
4890
|
},
|
|
3106
4891
|
async setWorkflow(workflowId) {
|
|
3107
4892
|
const id = normalizeWorkflowId(workflowId, DEFAULT_WORKFLOW_ID);
|
|
3108
4893
|
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
3109
4894
|
const pack = loadWorkflowPack(dataDir, id);
|
|
3110
4895
|
if (!pack || pack.id !== id) throw new Error(`workflow "${workflowId}" not found`);
|
|
3111
|
-
const nextConfig =
|
|
4896
|
+
const nextConfig = { ...config };
|
|
3112
4897
|
nextConfig.workflow = { ...(nextConfig.workflow || {}), active: id };
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
if (session?.id) {
|
|
3116
|
-
mgr.closeSession(session.id, 'cli-workflow-switch');
|
|
3117
|
-
session = null;
|
|
3118
|
-
}
|
|
3119
|
-
await recreateCurrentSessionIfReady();
|
|
3120
|
-
return { id: pack.id, name: pack.name, description: pack.description, source: pack.source };
|
|
4898
|
+
saveConfigAndAdopt(nextConfig);
|
|
4899
|
+
return workflowSummary(pack);
|
|
3121
4900
|
},
|
|
3122
4901
|
async setAgentRoute(agentId, next) {
|
|
3123
4902
|
const id = normalizeAgentId(agentId);
|
|
@@ -3127,12 +4906,12 @@ function parsedProviderModelVersion(id) {
|
|
|
3127
4906
|
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
3128
4907
|
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
3129
4908
|
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
3130
|
-
|
|
4909
|
+
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
3131
4910
|
|
|
3132
4911
|
const routeToSave = normalizeWorkflowRoute(selectedRoute);
|
|
3133
4912
|
if (!routeToSave) throw new Error('agent route requires provider and model');
|
|
3134
4913
|
const agent = FIXED_AGENT_SLOTS.find((item) => item.id === id);
|
|
3135
|
-
const nextConfig =
|
|
4914
|
+
const nextConfig = { ...config };
|
|
3136
4915
|
nextConfig.agents = {
|
|
3137
4916
|
...(nextConfig.agents || {}),
|
|
3138
4917
|
[id]: routeToSave,
|
|
@@ -3144,24 +4923,48 @@ function parsedProviderModelVersion(id) {
|
|
|
3144
4923
|
[agent.workflowSlot]: routeToSave,
|
|
3145
4924
|
};
|
|
3146
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.
|
|
3147
4928
|
nextConfig.maintenance = {
|
|
3148
4929
|
...(nextConfig.maintenance || {}),
|
|
3149
|
-
...(id === 'explore' ? { explore:
|
|
3150
|
-
...(id === 'maintainer' ? { memory:
|
|
4930
|
+
...(id === 'explore' ? { explore: routeToSave } : {}),
|
|
4931
|
+
...(id === 'maintainer' ? { memory: routeToSave } : {}),
|
|
3151
4932
|
};
|
|
3152
4933
|
}
|
|
3153
|
-
|
|
3154
|
-
config = cfgMod.loadConfig();
|
|
4934
|
+
saveConfigAndAdopt(nextConfig);
|
|
3155
4935
|
return routeToSave;
|
|
3156
4936
|
},
|
|
3157
4937
|
async ask(prompt, options = {}) {
|
|
3158
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
|
+
}
|
|
3159
4947
|
const startedAt = Date.now();
|
|
3160
4948
|
try {
|
|
3161
4949
|
await refreshSessionForCwdIfNeeded('cwd-change');
|
|
3162
4950
|
if (!session?.id) await createCurrentSession('turn');
|
|
3163
4951
|
hooks.emit('turn:start', { sessionId: session.id, prompt, cwd: currentCwd });
|
|
3164
|
-
|
|
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');
|
|
3165
4968
|
const result = await mgr.askSession(
|
|
3166
4969
|
session.id,
|
|
3167
4970
|
prompt,
|
|
@@ -3184,16 +4987,25 @@ function parsedProviderModelVersion(id) {
|
|
|
3184
4987
|
{
|
|
3185
4988
|
onTextDelta: options.onTextDelta,
|
|
3186
4989
|
onReasoningDelta: options.onReasoningDelta,
|
|
4990
|
+
onAssistantText: options.onAssistantText,
|
|
3187
4991
|
onUsageDelta: options.onUsageDelta,
|
|
3188
4992
|
onToolResult: options.onToolResult,
|
|
4993
|
+
onToolApproval: options.onToolApproval,
|
|
4994
|
+
onCompactEvent: options.onCompactEvent,
|
|
3189
4995
|
onStageChange: options.onStageChange,
|
|
3190
4996
|
onStreamDelta: options.onStreamDelta,
|
|
3191
4997
|
drainSteering: options.drainSteering,
|
|
3192
4998
|
onSteerMessage: options.onSteerMessage,
|
|
4999
|
+
notifyFn: notifyFnForSession(session.id),
|
|
3193
5000
|
},
|
|
3194
5001
|
);
|
|
3195
5002
|
session = mgr.getSession(session.id) || session;
|
|
3196
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 */ }
|
|
3197
5009
|
return { result, session };
|
|
3198
5010
|
} catch (error) {
|
|
3199
5011
|
hooks.emit('turn:error', { sessionId: session?.id || null, elapsedMs: Date.now() - startedAt, error: error?.message || String(error) });
|
|
@@ -3202,27 +5014,31 @@ function parsedProviderModelVersion(id) {
|
|
|
3202
5014
|
activeTurnCount = Math.max(0, activeTurnCount - 1);
|
|
3203
5015
|
if (!firstTurnCompleted) {
|
|
3204
5016
|
firstTurnCompleted = true;
|
|
5017
|
+
scheduleProviderWarmup();
|
|
3205
5018
|
scheduleProviderModelWarmup();
|
|
3206
5019
|
}
|
|
3207
5020
|
}
|
|
3208
5021
|
},
|
|
3209
5022
|
async clear(options = {}) {
|
|
3210
5023
|
if (!session?.id) return false;
|
|
3211
|
-
const cleared = await mgr.clearSessionMessages(session.id);
|
|
5024
|
+
const cleared = await mgr.clearSessionMessages(session.id, options);
|
|
3212
5025
|
if (!cleared) return false;
|
|
3213
5026
|
session = typeof cleared === 'object' ? cleared : (mgr.getSession(session.id) || session);
|
|
3214
|
-
if (options.
|
|
3215
|
-
try {
|
|
5027
|
+
if (options.recoverAgent === true) {
|
|
5028
|
+
try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
|
|
3216
5029
|
}
|
|
3217
5030
|
invalidateContextStatusCache();
|
|
3218
5031
|
return true;
|
|
3219
5032
|
},
|
|
3220
5033
|
async compact(options = {}) {
|
|
3221
5034
|
if (!session?.id) return null;
|
|
5035
|
+
if (activeTurnCount > 0) {
|
|
5036
|
+
return { changed: false, reason: 'compact skipped: turn in progress' };
|
|
5037
|
+
}
|
|
3222
5038
|
const result = await mgr.compactSessionMessages(session.id);
|
|
3223
5039
|
session = mgr.getSession(session.id) || session;
|
|
3224
|
-
if (options.
|
|
3225
|
-
try {
|
|
5040
|
+
if (options.recoverAgent === true) {
|
|
5041
|
+
try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
|
|
3226
5042
|
}
|
|
3227
5043
|
invalidateContextStatusCache();
|
|
3228
5044
|
return result;
|
|
@@ -3234,25 +5050,15 @@ function parsedProviderModelVersion(id) {
|
|
|
3234
5050
|
await recreateCurrentSessionIfReady();
|
|
3235
5051
|
return mode;
|
|
3236
5052
|
},
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
try { sharedCfgMod.updateSection('agent', (s) => ({ ...(s || {}), bridgeMode: applied })); } catch {}
|
|
3240
|
-
return applied;
|
|
3241
|
-
},
|
|
3242
|
-
toggleBridgeMode() {
|
|
3243
|
-
const applied = bridge.toggleDefaultMode();
|
|
3244
|
-
try { sharedCfgMod.updateSection('agent', (s) => ({ ...(s || {}), bridgeMode: applied })); } catch {}
|
|
3245
|
-
return applied;
|
|
3246
|
-
},
|
|
3247
|
-
bridgeStatus() {
|
|
3248
|
-
return bridgeStatusState();
|
|
5053
|
+
agentStatus() {
|
|
5054
|
+
return agentStatusState();
|
|
3249
5055
|
},
|
|
3250
5056
|
get clientHostPid() {
|
|
3251
5057
|
return session?.clientHostPid || process.pid;
|
|
3252
5058
|
},
|
|
3253
|
-
|
|
5059
|
+
agentControl(args = {}) {
|
|
3254
5060
|
const callerSessionId = session?.id || null;
|
|
3255
|
-
return
|
|
5061
|
+
return agentTool.execute(args, {
|
|
3256
5062
|
callerCwd: currentCwd,
|
|
3257
5063
|
invocationSource: 'user-command',
|
|
3258
5064
|
callerSessionId,
|
|
@@ -3282,6 +5088,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3282
5088
|
activeCount: rows.filter((row) => row.active).length,
|
|
3283
5089
|
tools,
|
|
3284
5090
|
activeTools: sortedNamesByMeasuredUsage(activeNames),
|
|
5091
|
+
discoveredTools: sortedNamesByMeasuredUsage(surface?.deferredDiscoveredTools || []),
|
|
3285
5092
|
};
|
|
3286
5093
|
},
|
|
3287
5094
|
selectTools(names) {
|
|
@@ -3297,7 +5104,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3297
5104
|
return mcpStatus();
|
|
3298
5105
|
},
|
|
3299
5106
|
async reconnectMcp() {
|
|
3300
|
-
|
|
5107
|
+
reloadFullConfig();
|
|
3301
5108
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3302
5109
|
invalidatePreSessionToolSurface();
|
|
3303
5110
|
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-reconnect');
|
|
@@ -3306,13 +5113,12 @@ function parsedProviderModelVersion(id) {
|
|
|
3306
5113
|
},
|
|
3307
5114
|
async addMcpServer(input = {}) {
|
|
3308
5115
|
const { name, config: serverConfig } = normalizeMcpServerInput(input);
|
|
3309
|
-
const nextConfig =
|
|
5116
|
+
const nextConfig = { ...config };
|
|
3310
5117
|
nextConfig.mcpServers = {
|
|
3311
5118
|
...(nextConfig.mcpServers || {}),
|
|
3312
5119
|
[name]: serverConfig,
|
|
3313
5120
|
};
|
|
3314
|
-
|
|
3315
|
-
config = cfgMod.loadConfig();
|
|
5121
|
+
saveConfigAndAdopt(nextConfig);
|
|
3316
5122
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3317
5123
|
invalidatePreSessionToolSurface();
|
|
3318
5124
|
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-add');
|
|
@@ -3322,7 +5128,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3322
5128
|
async removeMcpServer(name) {
|
|
3323
5129
|
const serverName = clean(name);
|
|
3324
5130
|
if (!serverName) throw new Error('MCP server name is required');
|
|
3325
|
-
const nextConfig =
|
|
5131
|
+
const nextConfig = { ...config };
|
|
3326
5132
|
const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
|
|
3327
5133
|
? { ...nextConfig.mcpServers }
|
|
3328
5134
|
: {};
|
|
@@ -3330,8 +5136,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3330
5136
|
throw new Error(`MCP server not configured: ${serverName}`);
|
|
3331
5137
|
}
|
|
3332
5138
|
delete current[serverName];
|
|
3333
|
-
|
|
3334
|
-
config = cfgMod.loadConfig();
|
|
5139
|
+
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
3335
5140
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3336
5141
|
invalidatePreSessionToolSurface();
|
|
3337
5142
|
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-remove');
|
|
@@ -3341,7 +5146,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3341
5146
|
async setMcpServerEnabled(name, enabled) {
|
|
3342
5147
|
const serverName = clean(name);
|
|
3343
5148
|
if (!serverName) throw new Error('MCP server name is required');
|
|
3344
|
-
const nextConfig =
|
|
5149
|
+
const nextConfig = { ...config };
|
|
3345
5150
|
const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
|
|
3346
5151
|
? { ...nextConfig.mcpServers }
|
|
3347
5152
|
: {};
|
|
@@ -3349,8 +5154,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3349
5154
|
throw new Error(`MCP server not configured: ${serverName}`);
|
|
3350
5155
|
}
|
|
3351
5156
|
current[serverName] = { ...(current[serverName] || {}), enabled: enabled !== false };
|
|
3352
|
-
|
|
3353
|
-
config = cfgMod.loadConfig();
|
|
5157
|
+
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
3354
5158
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3355
5159
|
invalidatePreSessionToolSurface();
|
|
3356
5160
|
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-toggle');
|
|
@@ -3401,13 +5205,12 @@ function parsedProviderModelVersion(id) {
|
|
|
3401
5205
|
const key = clean(plugin.id || plugin.name || plugin);
|
|
3402
5206
|
const dataDir = cfgMod.getPluginData?.();
|
|
3403
5207
|
const removed = registryRemovePlugin(key, { dataDir });
|
|
3404
|
-
const nextConfig =
|
|
5208
|
+
const nextConfig = { ...config };
|
|
3405
5209
|
const serverName = pluginMcpServerName(plugin);
|
|
3406
5210
|
if (nextConfig.mcpServers && Object.prototype.hasOwnProperty.call(nextConfig.mcpServers, serverName)) {
|
|
3407
5211
|
const current = { ...nextConfig.mcpServers };
|
|
3408
5212
|
delete current[serverName];
|
|
3409
|
-
|
|
3410
|
-
config = cfgMod.loadConfig();
|
|
5213
|
+
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
3411
5214
|
await connectConfiguredMcp({ reset: true });
|
|
3412
5215
|
invalidatePreSessionToolSurface();
|
|
3413
5216
|
}
|
|
@@ -3422,7 +5225,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3422
5225
|
const scriptPath = join(root, script);
|
|
3423
5226
|
if (!existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${scriptPath}`);
|
|
3424
5227
|
const serverName = pluginMcpServerName(plugin);
|
|
3425
|
-
const nextConfig =
|
|
5228
|
+
const nextConfig = { ...config };
|
|
3426
5229
|
nextConfig.mcpServers = {
|
|
3427
5230
|
...(nextConfig.mcpServers || {}),
|
|
3428
5231
|
[serverName]: {
|
|
@@ -3435,8 +5238,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3435
5238
|
},
|
|
3436
5239
|
},
|
|
3437
5240
|
};
|
|
3438
|
-
|
|
3439
|
-
config = cfgMod.loadConfig();
|
|
5241
|
+
saveConfigAndAdopt(nextConfig);
|
|
3440
5242
|
const status = await connectConfiguredMcp({ reset: true });
|
|
3441
5243
|
invalidatePreSessionToolSurface();
|
|
3442
5244
|
if (session?.id) mgr.closeSession(session.id, 'cli-plugin-mcp-enable');
|
|
@@ -3504,8 +5306,11 @@ function parsedProviderModelVersion(id) {
|
|
|
3504
5306
|
}
|
|
3505
5307
|
return result;
|
|
3506
5308
|
},
|
|
3507
|
-
async setRoute(next) {
|
|
5309
|
+
async setRoute(next, options = {}) {
|
|
5310
|
+
const applyToCurrentSession = options?.applyToCurrentSession !== false;
|
|
3508
5311
|
const requested = { ...(next || {}) };
|
|
5312
|
+
validateRequestedModelSelector(config, requested);
|
|
5313
|
+
const providerExplicitlyRequested = clean(next?.provider) !== '';
|
|
3509
5314
|
if (requested.effort === undefined && !requested.provider && !requested.model && hasOwn(route, 'effort')) {
|
|
3510
5315
|
requested.effort = route.effort;
|
|
3511
5316
|
}
|
|
@@ -3515,14 +5320,34 @@ function parsedProviderModelVersion(id) {
|
|
|
3515
5320
|
let selectedRoute = resolveRoute(config, requested);
|
|
3516
5321
|
await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
|
|
3517
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
|
+
}
|
|
3518
5335
|
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
3519
5336
|
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
3520
|
-
|
|
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
|
+
}
|
|
3521
5344
|
const leadRoute = persistLeadRoute(selectedRoute);
|
|
3522
5345
|
route = resolveRoute(config, leadRoute
|
|
3523
5346
|
? { model: workflowPresetId('lead') }
|
|
3524
5347
|
: selectedRoute);
|
|
3525
5348
|
await refreshRouteEffort(modelMeta);
|
|
5349
|
+
refreshStatuslineUsageSnapshot(route);
|
|
5350
|
+
scheduleStatuslineUsageRefresh();
|
|
3526
5351
|
if (session) {
|
|
3527
5352
|
const updated = mgr.updateSessionRoute?.(session.id, {
|
|
3528
5353
|
provider: route.provider,
|
|
@@ -3537,7 +5362,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3537
5362
|
session.fast = route.fast === true;
|
|
3538
5363
|
session.effort = route.effectiveEffort || null;
|
|
3539
5364
|
}
|
|
3540
|
-
statusRoutes
|
|
5365
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
3541
5366
|
invalidateContextStatusCache();
|
|
3542
5367
|
}
|
|
3543
5368
|
return route;
|
|
@@ -3551,14 +5376,14 @@ function parsedProviderModelVersion(id) {
|
|
|
3551
5376
|
throw new Error(`fast mode is not available for ${route.provider}/${route.model}`);
|
|
3552
5377
|
}
|
|
3553
5378
|
route = resolveRoute(config, { provider: route.provider, model: route.model, effort: route.effort, fast: fastCapable ? enabled : false });
|
|
3554
|
-
|
|
5379
|
+
adoptConfig(saveModelSettings(cfgMod, route, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
3555
5380
|
const leadRoute = persistLeadRoute(route);
|
|
3556
5381
|
if (leadRoute) route = resolveRoute(config, { model: workflowPresetId('lead') });
|
|
3557
5382
|
await refreshRouteEffort(modelMeta);
|
|
3558
5383
|
if (session) {
|
|
3559
5384
|
session.fast = route.fast === true;
|
|
3560
5385
|
session.effort = route.effectiveEffort || null;
|
|
3561
|
-
statusRoutes
|
|
5386
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
3562
5387
|
invalidateContextStatusCache();
|
|
3563
5388
|
}
|
|
3564
5389
|
return route.fast === true;
|
|
@@ -3571,15 +5396,17 @@ function parsedProviderModelVersion(id) {
|
|
|
3571
5396
|
async setEffort(value) {
|
|
3572
5397
|
const normalized = normalizeEffortInput(value);
|
|
3573
5398
|
route = { ...route, effort: normalized };
|
|
3574
|
-
|
|
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 });
|
|
3575
5402
|
const leadRoute = persistLeadRoute(route);
|
|
3576
5403
|
if (leadRoute) {
|
|
3577
5404
|
route = resolveRoute(config, { model: workflowPresetId('lead') });
|
|
3578
5405
|
}
|
|
3579
|
-
await refreshRouteEffort();
|
|
5406
|
+
await refreshRouteEffort(modelMeta);
|
|
3580
5407
|
if (session) {
|
|
3581
5408
|
session.effort = route.effectiveEffort || null;
|
|
3582
|
-
statusRoutes
|
|
5409
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
3583
5410
|
invalidateContextStatusCache();
|
|
3584
5411
|
}
|
|
3585
5412
|
return route;
|
|
@@ -3587,10 +5414,18 @@ function parsedProviderModelVersion(id) {
|
|
|
3587
5414
|
async close(reason = 'cli-exit', options = {}) {
|
|
3588
5415
|
const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
|
|
3589
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 {}
|
|
3590
5421
|
if (channelStartTimer) {
|
|
3591
5422
|
clearTimeout(channelStartTimer);
|
|
3592
5423
|
channelStartTimer = null;
|
|
3593
5424
|
}
|
|
5425
|
+
if (providerSetupWarmupTimer) {
|
|
5426
|
+
clearTimeout(providerSetupWarmupTimer);
|
|
5427
|
+
providerSetupWarmupTimer = null;
|
|
5428
|
+
}
|
|
3594
5429
|
if (providerWarmupTimer) {
|
|
3595
5430
|
clearTimeout(providerWarmupTimer);
|
|
3596
5431
|
providerWarmupTimer = null;
|
|
@@ -3599,9 +5434,21 @@ function parsedProviderModelVersion(id) {
|
|
|
3599
5434
|
clearTimeout(providerModelWarmupTimer);
|
|
3600
5435
|
providerModelWarmupTimer = null;
|
|
3601
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
|
+
}
|
|
3602
5449
|
try { cancelBackgroundTasks({ reason, notify: false }); } catch {}
|
|
3603
5450
|
const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
|
|
3604
|
-
try {
|
|
5451
|
+
try { agentTool.closeAll(reason); } catch {}
|
|
3605
5452
|
let mcpStop = null;
|
|
3606
5453
|
try { mcpStop = mcpClient.disconnectAll?.(); } catch {}
|
|
3607
5454
|
const openaiWsStop = globalThis.__mixdogOpenaiWsRuntimeLoaded === true
|
|
@@ -3610,14 +5457,35 @@ function parsedProviderModelVersion(id) {
|
|
|
3610
5457
|
.catch(() => {})
|
|
3611
5458
|
: null;
|
|
3612
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;
|
|
3613
5468
|
let ok = false;
|
|
3614
5469
|
if (session?.id) {
|
|
3615
5470
|
statusRoutes?.clearGatewaySessionRoute?.(session.id);
|
|
3616
5471
|
ok = mgr.closeSession(session.id, reason);
|
|
3617
5472
|
session = null;
|
|
3618
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;
|
|
3619
5484
|
if (detach) {
|
|
3620
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 {}
|
|
3621
5489
|
for (const stop of [mcpStop, openaiWsStop, patchStop]) {
|
|
3622
5490
|
Promise.resolve(stop).catch(() => {});
|
|
3623
5491
|
}
|
|
@@ -3628,6 +5496,9 @@ function parsedProviderModelVersion(id) {
|
|
|
3628
5496
|
withTeardownDeadline(mcpStop, 1500, false),
|
|
3629
5497
|
withTeardownDeadline(openaiWsStop, 1500, false),
|
|
3630
5498
|
withTeardownDeadline(patchStop, 1500, false),
|
|
5499
|
+
withTeardownDeadline(memoryStop, 5500, false),
|
|
5500
|
+
withTeardownDeadline(shellJobsStop, 1500, false),
|
|
5501
|
+
withTeardownDeadline(bashSessionsStop, 1500, false),
|
|
3631
5502
|
]);
|
|
3632
5503
|
return ok;
|
|
3633
5504
|
},
|
|
@@ -3645,23 +5516,27 @@ function parsedProviderModelVersion(id) {
|
|
|
3645
5516
|
const leadish = role === 'lead'
|
|
3646
5517
|
|| sourceType === 'lead'
|
|
3647
5518
|
|| (sourceType === 'cli' && (!sourceName || sourceName === 'main'))
|
|
3648
|
-
|| (!sourceType && !sourceName && owner
|
|
5519
|
+
|| (!sourceType && !sourceName && !isAgentOwner(owner));
|
|
3649
5520
|
if (!leadish) return null;
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
.
|
|
3654
|
-
|
|
3655
|
-
|
|
5521
|
+
let preview = cleanSessionPreview(s.preview || '');
|
|
5522
|
+
let messageCount = Math.max(0, Number(s.messageCount) || 0);
|
|
5523
|
+
if (!preview && Array.isArray(s.messages)) {
|
|
5524
|
+
const msgs = s.messages || [];
|
|
5525
|
+
const userPreviews = msgs
|
|
5526
|
+
.filter(m => m && m.role === 'user')
|
|
5527
|
+
.map(m => cleanSessionPreview(sessionMessageText(m.content)))
|
|
5528
|
+
.filter(text => !isSessionPreviewNoise(text));
|
|
5529
|
+
preview = userPreviews[userPreviews.length - 1] || userPreviews[0] || '';
|
|
5530
|
+
messageCount = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant')).length;
|
|
5531
|
+
}
|
|
3656
5532
|
if (!preview) return null;
|
|
3657
|
-
const userAsst = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant'));
|
|
3658
5533
|
return {
|
|
3659
5534
|
id: s.id,
|
|
3660
5535
|
updatedAt: s.updatedAt,
|
|
3661
5536
|
cwd: s.cwd || '',
|
|
3662
5537
|
model: s.model,
|
|
3663
5538
|
provider: s.provider,
|
|
3664
|
-
messageCount
|
|
5539
|
+
messageCount,
|
|
3665
5540
|
preview,
|
|
3666
5541
|
};
|
|
3667
5542
|
}).filter(Boolean);
|
|
@@ -3687,11 +5562,11 @@ function parsedProviderModelVersion(id) {
|
|
|
3687
5562
|
await refreshRouteEffort();
|
|
3688
5563
|
session.effort = route.effectiveEffort || null;
|
|
3689
5564
|
session.cwd = currentCwd;
|
|
3690
|
-
applyDeferredToolSurface(session, mode, standaloneTools);
|
|
5565
|
+
applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
3691
5566
|
invalidatePreSessionToolSurface();
|
|
3692
5567
|
invalidateContextStatusCache();
|
|
3693
5568
|
sessionNeedsCwdRefresh = false;
|
|
3694
|
-
statusRoutes
|
|
5569
|
+
writeStatuslineRoute(statusRoutes, session, route);
|
|
3695
5570
|
return {
|
|
3696
5571
|
id: resumed.id,
|
|
3697
5572
|
messages: resumed.messages || [],
|