mixdog 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +355 -22
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +80 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +75 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { performance } from 'node:perf_hooks';
|
|
6
6
|
import { ensureStandaloneEnvironment } from './standalone/seeds.mjs';
|
|
@@ -12,10 +12,17 @@ import { createStandaloneMemoryRuntime } from './standalone/memory-runtime-proxy
|
|
|
12
12
|
import { createStandaloneHookBus } from './standalone/hook-bus.mjs';
|
|
13
13
|
import { writeLastSessionCwd } from './runtime/shared/user-cwd.mjs';
|
|
14
14
|
import { cancelBackgroundTasks } from './runtime/shared/background-tasks.mjs';
|
|
15
|
+
import { createTranscriptWriter } from './runtime/shared/transcript-writer.mjs';
|
|
16
|
+
import { mixdogHome } from './runtime/shared/plugin-paths.mjs';
|
|
17
|
+
import { checkLatestVersion, runGlobalUpdate, localPackageVersion } from './runtime/shared/update-checker.mjs';
|
|
15
18
|
import {
|
|
16
19
|
modelVisibleToolCompletionMessage,
|
|
17
20
|
shouldPersistModelVisibleToolCompletion,
|
|
18
21
|
} from './runtime/shared/tool-execution-contract.mjs';
|
|
22
|
+
import {
|
|
23
|
+
channelNotificationModelContent,
|
|
24
|
+
shouldMirrorChannelNotificationToPending,
|
|
25
|
+
} from './runtime/shared/channel-notification-routing.mjs';
|
|
19
26
|
import {
|
|
20
27
|
normalizeAgentPermissionOrNone,
|
|
21
28
|
readMarkdownDocument,
|
|
@@ -25,17 +32,22 @@ import {
|
|
|
25
32
|
PROVIDER_STATUS_TOOL,
|
|
26
33
|
beginOAuthProviderLogin,
|
|
27
34
|
forgetProviderAuth,
|
|
35
|
+
isKnownProvider,
|
|
28
36
|
loginOAuthProvider,
|
|
29
37
|
providerSetup,
|
|
30
38
|
renderProviderStatus,
|
|
31
39
|
saveOpenAIUsageSessionKey,
|
|
32
40
|
saveOpenCodeGoUsageAuth,
|
|
41
|
+
loginOpenCodeGoUsage,
|
|
33
42
|
saveProviderApiKey,
|
|
34
43
|
setLocalProvider,
|
|
35
44
|
} from './standalone/provider-admin.mjs';
|
|
36
45
|
import { createUsageDashboard } from './standalone/usage-dashboard.mjs';
|
|
37
46
|
import { fetchOAuthUsageSnapshot } from './runtime/agent/orchestrator/providers/oauth-usage.mjs';
|
|
38
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
getModelMetadataSync,
|
|
49
|
+
warmCatalogsInBackground,
|
|
50
|
+
} from './runtime/agent/orchestrator/providers/model-catalog.mjs';
|
|
39
51
|
import {
|
|
40
52
|
isResponsesFreeformTool,
|
|
41
53
|
toResponsesCustomTool,
|
|
@@ -46,13 +58,16 @@ import {
|
|
|
46
58
|
deleteSchedule,
|
|
47
59
|
deleteWebhook,
|
|
48
60
|
forgetDiscordToken,
|
|
61
|
+
forgetTelegramToken,
|
|
49
62
|
forgetWebhookAuthtoken,
|
|
50
63
|
renderChannelStatus,
|
|
51
64
|
saveChannel,
|
|
52
65
|
saveDiscordToken,
|
|
66
|
+
saveTelegramToken,
|
|
53
67
|
saveSchedule,
|
|
54
68
|
saveWebhook,
|
|
55
69
|
saveWebhookAuthtoken,
|
|
70
|
+
setBackend,
|
|
56
71
|
setScheduleEnabled,
|
|
57
72
|
setWebhookEnabled,
|
|
58
73
|
setWebhookConfig,
|
|
@@ -69,233 +84,130 @@ import {
|
|
|
69
84
|
estimateRequestReserveTokens,
|
|
70
85
|
estimateTranscriptContextUsage,
|
|
71
86
|
estimateToolSchemaTokens,
|
|
87
|
+
resolveCompactBufferTokens,
|
|
88
|
+
resolveCompactTriggerTokens,
|
|
89
|
+
summarizeContextMessages,
|
|
72
90
|
} from './runtime/agent/orchestrator/session/context-utils.mjs';
|
|
73
91
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
204
|
-
function summarizeContextMessages(messages) {
|
|
205
|
-
const rows = {
|
|
206
|
-
system: { count: 0, tokens: 0 },
|
|
207
|
-
user: { count: 0, tokens: 0 },
|
|
208
|
-
assistant: { count: 0, tokens: 0 },
|
|
209
|
-
tool: { count: 0, tokens: 0 },
|
|
210
|
-
other: { count: 0, tokens: 0 },
|
|
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
|
-
};
|
|
224
|
-
let toolCallCount = 0;
|
|
225
|
-
let toolCallTokens = 0;
|
|
226
|
-
let toolResultCount = 0;
|
|
227
|
-
let toolResultTokens = 0;
|
|
228
|
-
for (const message of messages || []) {
|
|
229
|
-
const role = rows[message?.role] ? message.role : 'other';
|
|
230
|
-
const text = messageContextText(message);
|
|
231
|
-
const tokens = roughTokenCount(text) + 4;
|
|
232
|
-
rows[role].count += 1;
|
|
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
|
-
}
|
|
260
|
-
if (message?.role === 'assistant' && Array.isArray(message.toolCalls) && message.toolCalls.length) {
|
|
261
|
-
toolCallCount += message.toolCalls.length;
|
|
262
|
-
try { toolCallTokens += roughTokenCount(JSON.stringify(message.toolCalls)); }
|
|
263
|
-
catch { toolCallTokens += roughTokenCount(`[${message.toolCalls.length} tool calls]`); }
|
|
264
|
-
}
|
|
265
|
-
if (message?.role === 'tool') {
|
|
266
|
-
toolResultCount += 1;
|
|
267
|
-
toolResultTokens += tokens;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return {
|
|
271
|
-
count: Array.isArray(messages) ? messages.length : 0,
|
|
272
|
-
estimatedTokens: Array.isArray(messages) ? estimateMessagesTokens(messages) : 0,
|
|
273
|
-
roles: rows,
|
|
274
|
-
semantic,
|
|
275
|
-
toolCallCount,
|
|
276
|
-
toolCallTokens,
|
|
277
|
-
toolResultCount,
|
|
278
|
-
toolResultTokens,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function isSessionPreviewNoise(text) {
|
|
283
|
-
const value = String(text || '').trim();
|
|
284
|
-
return !value
|
|
285
|
-
|| value.startsWith('<system-reminder>')
|
|
286
|
-
|| value.startsWith('</system-reminder>')
|
|
287
|
-
|| /^#\s*permission\b/i.test(value)
|
|
288
|
-
|| /^permission:\s*/i.test(value)
|
|
289
|
-
|| /^cwd:\s*/i.test(value);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function cleanSessionPreview(text) {
|
|
293
|
-
return String(text || '')
|
|
294
|
-
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, ' ')
|
|
295
|
-
.replace(/\s+/g, ' ')
|
|
296
|
-
.trim()
|
|
297
|
-
.slice(0, 160);
|
|
298
|
-
}
|
|
92
|
+
import {
|
|
93
|
+
sessionMessageText,
|
|
94
|
+
messageContextText,
|
|
95
|
+
isSessionPreviewNoise,
|
|
96
|
+
cleanSessionPreview,
|
|
97
|
+
clean,
|
|
98
|
+
hasOwn,
|
|
99
|
+
toolResponseText,
|
|
100
|
+
isEmptyRecallText,
|
|
101
|
+
currentSessionRecallRows,
|
|
102
|
+
sessionHasConversationMessages,
|
|
103
|
+
} from './session-runtime/session-text.mjs';
|
|
104
|
+
import {
|
|
105
|
+
TOOL_MODES,
|
|
106
|
+
ALL_EFFORT_LEVELS,
|
|
107
|
+
EFFORT_LABELS,
|
|
108
|
+
EFFORT_OPTIONS_BY_PROVIDER,
|
|
109
|
+
EFFORT_BY_FAMILY,
|
|
110
|
+
EFFORT_FALLBACKS,
|
|
111
|
+
normalizeToolMode,
|
|
112
|
+
normalizeEffortInput,
|
|
113
|
+
effortOptionsFor,
|
|
114
|
+
coerceEffortFor,
|
|
115
|
+
normalizeSavedEffort,
|
|
116
|
+
effortItemsFor,
|
|
117
|
+
toolSpecForMode,
|
|
118
|
+
deferredSurfaceModeForLead,
|
|
119
|
+
} from './session-runtime/effort.mjs';
|
|
120
|
+
import {
|
|
121
|
+
LAZY_SECRET_PROVIDERS,
|
|
122
|
+
routeFastKey,
|
|
123
|
+
fastCapableFor,
|
|
124
|
+
makeSearchCapableFor,
|
|
125
|
+
fastPreferenceFor,
|
|
126
|
+
saveModelSettings,
|
|
127
|
+
} from './session-runtime/model-capabilities.mjs';
|
|
128
|
+
import {
|
|
129
|
+
DEFAULT_PROVIDER,
|
|
130
|
+
DEFAULT_MODEL,
|
|
131
|
+
makeResolveDefaultProvider,
|
|
132
|
+
findPreset,
|
|
133
|
+
makeResolveRoute,
|
|
134
|
+
isLikelyRawModelId,
|
|
135
|
+
validateRequestedModelSelector,
|
|
136
|
+
ensureProviderEnabled,
|
|
137
|
+
normalizeSystemShellConfig,
|
|
138
|
+
normalizeSystemShellCommand,
|
|
139
|
+
normalizeAutoClearConfig,
|
|
140
|
+
normalizeCompactionConfig,
|
|
141
|
+
moduleEnabled,
|
|
142
|
+
setModuleEnabledInConfig,
|
|
143
|
+
formatDurationMs,
|
|
144
|
+
parseDurationMs,
|
|
145
|
+
modelMetaLooksResolved,
|
|
146
|
+
modelSettingsFor,
|
|
147
|
+
normalizeCompactTypeSetting,
|
|
148
|
+
} from './session-runtime/config-helpers.mjs';
|
|
149
|
+
import {
|
|
150
|
+
routeForStatusline,
|
|
151
|
+
writeStatuslineRoute,
|
|
152
|
+
} from './session-runtime/statusline-route.mjs';
|
|
153
|
+
import {
|
|
154
|
+
normalizeOutputStyleId,
|
|
155
|
+
listOutputStyleCatalog,
|
|
156
|
+
findOutputStyle,
|
|
157
|
+
outputStyleStatus as outputStyleStatusRaw,
|
|
158
|
+
} from './session-runtime/output-styles.mjs';
|
|
159
|
+
import { readJsonSafe, readTextSafe } from './session-runtime/fs-utils.mjs';
|
|
160
|
+
import {
|
|
161
|
+
readProjectMcpServers,
|
|
162
|
+
countSkillFiles,
|
|
163
|
+
mcpScriptForPlugin,
|
|
164
|
+
normalizePluginMcpServerConfig,
|
|
165
|
+
pluginManifest,
|
|
166
|
+
pluginMcpServerName,
|
|
167
|
+
} from './session-runtime/plugin-mcp.mjs';
|
|
168
|
+
import {
|
|
169
|
+
WORKFLOW_ROUTE_SLOTS,
|
|
170
|
+
FIXED_AGENT_SLOTS,
|
|
171
|
+
SEARCH_DEFAULT_PROVIDER,
|
|
172
|
+
SEARCH_DEFAULT_MODEL,
|
|
173
|
+
workflowPresetId,
|
|
174
|
+
agentPresetSlot,
|
|
175
|
+
normalizeAgentId,
|
|
176
|
+
normalizeWorkflowId,
|
|
177
|
+
createWorkflowHelpers,
|
|
178
|
+
normalizeSearchProviderId,
|
|
179
|
+
isDefaultSearchRouteConfig,
|
|
180
|
+
isSearchCapableProvider,
|
|
181
|
+
normalizeSearchRouteConfig,
|
|
182
|
+
normalizeWorkflowRoute,
|
|
183
|
+
upsertWorkflowPreset,
|
|
184
|
+
createWorkflowRouteHelpers,
|
|
185
|
+
} from './session-runtime/workflow.mjs';
|
|
186
|
+
import {
|
|
187
|
+
MEASURED_TOOL_USAGE,
|
|
188
|
+
DEFERRED_DEFAULT_FULL_TOOLS,
|
|
189
|
+
DEFERRED_DEFAULT_READONLY_TOOLS,
|
|
190
|
+
DEFERRED_DEFAULT_LEAD_TOOLS,
|
|
191
|
+
toolKind,
|
|
192
|
+
toolSchemaBucket,
|
|
193
|
+
estimateToolSchemaBreakdown,
|
|
194
|
+
measuredToolUsage,
|
|
195
|
+
parseToolSelection,
|
|
196
|
+
parseToolSearchQuerySelection,
|
|
197
|
+
sortedCatalogByMeasuredUsage,
|
|
198
|
+
filterDisallowedTools,
|
|
199
|
+
sortedNamesByMeasuredUsage,
|
|
200
|
+
defaultDeferredToolNames,
|
|
201
|
+
compactToolSearchDescription,
|
|
202
|
+
toolRow,
|
|
203
|
+
toolSearchMatches,
|
|
204
|
+
applyDeferredToolSurface,
|
|
205
|
+
selectDeferredTools,
|
|
206
|
+
renderToolSearch,
|
|
207
|
+
} from './session-runtime/tool-catalog.mjs';
|
|
208
|
+
// Re-exported for external consumers (scripts/tool-smoke.mjs) that imported
|
|
209
|
+
// these from this module before the tool-catalog extraction.
|
|
210
|
+
export { defaultDeferredToolNames, compactToolSearchDescription } from './session-runtime/tool-catalog.mjs';
|
|
299
211
|
|
|
300
212
|
const RUNTIME = './runtime/agent/orchestrator';
|
|
301
213
|
const SEARCH_RUNTIME = './runtime/search/index.mjs';
|
|
@@ -315,18 +227,9 @@ const STANDALONE_ROOT = STANDALONE_SOURCE_ROOT;
|
|
|
315
227
|
const MIXDOG_HOME = process.env.MIXDOG_HOME || join(homedir(), '.mixdog');
|
|
316
228
|
const STANDALONE_DATA_DIR = process.env.MIXDOG_DATA_DIR || join(MIXDOG_HOME, 'data');
|
|
317
229
|
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
const ALL_EFFORT_LEVELS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
322
|
-
const EFFORT_LABELS = {
|
|
323
|
-
none: 'None',
|
|
324
|
-
low: 'Low',
|
|
325
|
-
medium: 'Medium',
|
|
326
|
-
high: 'High',
|
|
327
|
-
xhigh: 'Extra High',
|
|
328
|
-
max: 'Max',
|
|
329
|
-
};
|
|
230
|
+
const resolveDefaultProvider = makeResolveDefaultProvider(isKnownProvider);
|
|
231
|
+
const resolveRoute = makeResolveRoute(resolveDefaultProvider);
|
|
232
|
+
const searchCapableFor = makeSearchCapableFor(normalizeSearchProviderId, isSearchCapableProvider);
|
|
330
233
|
|
|
331
234
|
function envFlag(name) {
|
|
332
235
|
return /^(1|true|yes|on)$/i.test(String(process.env[name] || ''));
|
|
@@ -373,37 +276,6 @@ async function profiledImport(label, spec, { optional = false } = {}) {
|
|
|
373
276
|
throw error;
|
|
374
277
|
}
|
|
375
278
|
}
|
|
376
|
-
const EFFORT_OPTIONS_BY_PROVIDER = {
|
|
377
|
-
openai: ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
378
|
-
'openai-oauth': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
379
|
-
anthropic: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
380
|
-
'anthropic-oauth': ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
381
|
-
xai: ['none', 'low', 'medium', 'high'],
|
|
382
|
-
'grok-oauth': ['none', 'low', 'medium', 'high'],
|
|
383
|
-
'opencode-go': ['high', 'max'],
|
|
384
|
-
};
|
|
385
|
-
const EFFORT_BY_FAMILY = {
|
|
386
|
-
opus: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
387
|
-
sonnet: ['low', 'medium', 'high'],
|
|
388
|
-
haiku: [],
|
|
389
|
-
'gpt-5.5': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
390
|
-
'gpt-5.4': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
391
|
-
'gpt-5.2': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
392
|
-
'gpt-5': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
393
|
-
'gpt-mini': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
394
|
-
'gpt-nano': ['none', 'low', 'medium', 'high'],
|
|
395
|
-
'gpt-codex': ['none', 'low', 'medium', 'high'],
|
|
396
|
-
grok: ['none', 'low', 'medium', 'high'],
|
|
397
|
-
};
|
|
398
|
-
const EFFORT_FALLBACKS = {
|
|
399
|
-
max: ['max', 'xhigh', 'high', 'medium', 'low'],
|
|
400
|
-
xhigh: ['xhigh', 'high', 'medium', 'low'],
|
|
401
|
-
high: ['high', 'medium', 'low'],
|
|
402
|
-
medium: ['medium', 'low'],
|
|
403
|
-
low: ['low'],
|
|
404
|
-
none: ['none'],
|
|
405
|
-
};
|
|
406
|
-
|
|
407
279
|
export const TOOL_SEARCH_TOOL = {
|
|
408
280
|
name: 'tool_search',
|
|
409
281
|
title: 'Tool Search',
|
|
@@ -486,89 +358,10 @@ export const SKILL_TOOL = {
|
|
|
486
358
|
},
|
|
487
359
|
};
|
|
488
360
|
|
|
489
|
-
const MEASURED_TOOL_USAGE = Object.freeze({
|
|
490
|
-
read: 710,
|
|
491
|
-
code_graph: 520,
|
|
492
|
-
grep: 500,
|
|
493
|
-
find: 480,
|
|
494
|
-
glob: 460,
|
|
495
|
-
list: 430,
|
|
496
|
-
apply_patch: 400,
|
|
497
|
-
explore: 360,
|
|
498
|
-
agent: 330,
|
|
499
|
-
shell: 81,
|
|
500
|
-
cwd: 2,
|
|
501
|
-
diagnostics: 2,
|
|
502
|
-
recall: 2,
|
|
503
|
-
search: 2,
|
|
504
|
-
web_fetch: 2,
|
|
505
|
-
provider_status: 2,
|
|
506
|
-
channel_status: 2,
|
|
507
|
-
});
|
|
508
|
-
const MEASURED_TOOL_ORDER = Object.freeze(Object.keys(MEASURED_TOOL_USAGE));
|
|
509
361
|
const LEAD_DISALLOWED_TOOLS = Object.freeze(['diagnostics', 'open_config']);
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
'grep',
|
|
514
|
-
'find',
|
|
515
|
-
'glob',
|
|
516
|
-
'list',
|
|
517
|
-
'explore',
|
|
518
|
-
'apply_patch',
|
|
519
|
-
'Skill',
|
|
520
|
-
'tool_search',
|
|
521
|
-
]);
|
|
522
|
-
const DEFERRED_DEFAULT_READONLY_TOOLS = Object.freeze([
|
|
523
|
-
'read',
|
|
524
|
-
'code_graph',
|
|
525
|
-
'grep',
|
|
526
|
-
'find',
|
|
527
|
-
'glob',
|
|
528
|
-
'list',
|
|
529
|
-
'explore',
|
|
530
|
-
'Skill',
|
|
531
|
-
'tool_search',
|
|
532
|
-
]);
|
|
533
|
-
const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
|
|
534
|
-
'read',
|
|
535
|
-
'code_graph',
|
|
536
|
-
'grep',
|
|
537
|
-
'find',
|
|
538
|
-
'glob',
|
|
539
|
-
'list',
|
|
540
|
-
'shell',
|
|
541
|
-
'task',
|
|
542
|
-
'explore',
|
|
543
|
-
'apply_patch',
|
|
544
|
-
'agent',
|
|
545
|
-
'recall',
|
|
546
|
-
'search',
|
|
547
|
-
'web_fetch',
|
|
548
|
-
'cwd',
|
|
549
|
-
'Skill',
|
|
550
|
-
'tool_search',
|
|
551
|
-
]);
|
|
552
|
-
const READONLY_TOOL_NAMES = new Set([
|
|
553
|
-
'read',
|
|
554
|
-
'list',
|
|
555
|
-
'grep',
|
|
556
|
-
'find',
|
|
557
|
-
'glob',
|
|
558
|
-
'code_graph',
|
|
559
|
-
'search',
|
|
560
|
-
'web_fetch',
|
|
561
|
-
'recall',
|
|
562
|
-
'memory',
|
|
563
|
-
'provider_status',
|
|
564
|
-
'channel_status',
|
|
565
|
-
'schedule_status',
|
|
566
|
-
'fetch',
|
|
567
|
-
'Skill',
|
|
568
|
-
]);
|
|
569
|
-
const AGENT_HIDDEN_WRAPPER_TOOLS = new Set(['search']);
|
|
570
|
-
|
|
571
|
-
function applyStandaloneToolDefaults(tool) {
|
|
362
|
+
const AGENT_HIDDEN_WRAPPER_TOOLS = new Set([]);
|
|
363
|
+
|
|
364
|
+
export function __applyStandaloneToolDefaultsForTest(tool) {
|
|
572
365
|
if (!tool || !AGENT_HIDDEN_WRAPPER_TOOLS.has(tool.name)) return tool;
|
|
573
366
|
return {
|
|
574
367
|
...tool,
|
|
@@ -578,712 +371,10 @@ function applyStandaloneToolDefaults(tool) {
|
|
|
578
371
|
},
|
|
579
372
|
};
|
|
580
373
|
}
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
search: ['search', 'web_fetch'],
|
|
584
|
-
web: ['web_fetch', 'search'],
|
|
585
|
-
memory: ['memory', 'recall'],
|
|
586
|
-
channels: ['reply', 'fetch', 'react', 'edit_message', 'download_attachment', 'schedule_status', 'trigger_schedule', 'schedule_control', 'reload_config'],
|
|
587
|
-
discord: ['reply', 'fetch', 'react', 'edit_message', 'download_attachment'],
|
|
588
|
-
providers: ['provider_status'],
|
|
589
|
-
provider: ['provider_status'],
|
|
590
|
-
status: ['provider_status', 'channel_status', 'schedule_status'],
|
|
591
|
-
schedule: ['schedule_status', 'trigger_schedule', 'schedule_control'],
|
|
592
|
-
channel: ['channel_status'],
|
|
593
|
-
explore: ['explore'],
|
|
594
|
-
discovery: ['explore'],
|
|
595
|
-
agent: ['agent'],
|
|
596
|
-
graph: ['code_graph'],
|
|
597
|
-
code: ['code_graph'],
|
|
598
|
-
shell: ['shell', 'task'],
|
|
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
|
-
});
|
|
657
|
-
|
|
658
|
-
function normalizeToolMode(mode) {
|
|
659
|
-
const value = String(mode || '').trim().toLowerCase();
|
|
660
|
-
return TOOL_MODES.has(value) ? value : 'full';
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function normalizeEffortInput(value) {
|
|
664
|
-
const v = clean(value).toLowerCase();
|
|
665
|
-
if (!v || v === 'auto') return null;
|
|
666
|
-
if (!ALL_EFFORT_LEVELS.has(v)) {
|
|
667
|
-
throw new Error(`effort must be one of auto, ${[...ALL_EFFORT_LEVELS].join(', ')}`);
|
|
668
|
-
}
|
|
669
|
-
return v;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
function effortOptionsFor(provider, model) {
|
|
673
|
-
const providerAllowed = EFFORT_OPTIONS_BY_PROVIDER[provider] || null;
|
|
674
|
-
const filterProvider = (values) => {
|
|
675
|
-
const unique = [...new Set((values || []).map(clean).filter(Boolean))];
|
|
676
|
-
return providerAllowed ? unique.filter((v) => providerAllowed.includes(v)) : unique;
|
|
677
|
-
};
|
|
678
|
-
const declared = Array.isArray(model?.reasoningLevels)
|
|
679
|
-
? model.reasoningLevels.map(clean).filter(Boolean)
|
|
680
|
-
: [];
|
|
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);
|
|
696
|
-
if (Object.prototype.hasOwnProperty.call(EFFORT_BY_FAMILY, family)) {
|
|
697
|
-
return filterProvider(EFFORT_BY_FAMILY[family]);
|
|
698
|
-
}
|
|
699
|
-
return providerAllowed || [];
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function coerceEffortFor(provider, model, effort) {
|
|
703
|
-
if (!effort) return null;
|
|
704
|
-
const allowed = effortOptionsFor(provider, model);
|
|
705
|
-
if (!allowed || allowed.length === 0) return null;
|
|
706
|
-
if (allowed.includes(effort)) return effort;
|
|
707
|
-
for (const candidate of EFFORT_FALLBACKS[effort] || []) {
|
|
708
|
-
if (allowed.includes(candidate)) return candidate;
|
|
709
|
-
}
|
|
710
|
-
return null;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function hasOwn(obj, key) {
|
|
714
|
-
return Object.prototype.hasOwnProperty.call(obj || {}, key);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function modelSettingsFor(config, provider, model) {
|
|
718
|
-
const key = routeFastKey(provider, model);
|
|
719
|
-
const value = key ? config?.modelSettings?.[key] : null;
|
|
720
|
-
return value && typeof value === 'object' ? value : {};
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function normalizeSavedEffort(value) {
|
|
724
|
-
try {
|
|
725
|
-
return normalizeEffortInput(value);
|
|
726
|
-
} catch {
|
|
727
|
-
return null;
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function effortItemsFor(provider, model, activeEffort) {
|
|
732
|
-
const allowed = effortOptionsFor(provider, model);
|
|
733
|
-
const items = [];
|
|
734
|
-
for (const value of allowed || []) {
|
|
735
|
-
items.push({
|
|
736
|
-
value,
|
|
737
|
-
label: EFFORT_LABELS[value] || value,
|
|
738
|
-
description: value === activeEffort ? 'current' : '',
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
return items;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function toolSpecForMode(mode) {
|
|
745
|
-
return mode === 'readonly' ? ['tools:readonly'] : 'full';
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function deferredSurfaceModeForLead(mode) {
|
|
749
|
-
return mode === 'readonly' ? 'readonly' : 'lead';
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function clean(value) {
|
|
753
|
-
return String(value ?? '').trim();
|
|
754
|
-
}
|
|
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
|
-
|
|
765
|
-
const OUTPUT_STYLE_ORDER = ['default', 'simple', 'extreme-simple'];
|
|
766
|
-
const OUTPUT_STYLE_ALIASES = new Map([
|
|
767
|
-
['compact', 'default'],
|
|
768
|
-
['normal', 'default'],
|
|
769
|
-
['extreme', 'extreme-simple'],
|
|
770
|
-
['extremesimple', 'extreme-simple'],
|
|
771
|
-
['extreme-simple', 'extreme-simple'],
|
|
772
|
-
['extreme_simple', 'extreme-simple'],
|
|
773
|
-
]);
|
|
774
|
-
|
|
775
|
-
function normalizeOutputStyleId(value) {
|
|
776
|
-
const raw = clean(value).toLowerCase();
|
|
777
|
-
if (!raw) return '';
|
|
778
|
-
const slug = raw.replace(/[_\s]+/g, '-').replace(/^-+|-+$/g, '');
|
|
779
|
-
const compact = slug.replace(/[_.-]+/g, '');
|
|
780
|
-
if (OUTPUT_STYLE_ALIASES.has(slug)) return OUTPUT_STYLE_ALIASES.get(slug);
|
|
781
|
-
if (OUTPUT_STYLE_ALIASES.has(compact)) return OUTPUT_STYLE_ALIASES.get(compact);
|
|
782
|
-
return /^[a-z0-9.-]+$/.test(slug) ? slug : '';
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function outputStyleCompactKey(value) {
|
|
786
|
-
return normalizeOutputStyleId(value).replace(/[_.-]+/g, '');
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
function titleCaseOutputStyle(id) {
|
|
790
|
-
return clean(id)
|
|
791
|
-
.split(/[_.-]+/)
|
|
792
|
-
.filter(Boolean)
|
|
793
|
-
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
794
|
-
.join(' ') || 'Default';
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
function parseOutputStyleFrontmatter(markdown) {
|
|
798
|
-
const match = String(markdown || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
799
|
-
const meta = {};
|
|
800
|
-
if (!match) return meta;
|
|
801
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
802
|
-
const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
|
|
803
|
-
if (!kv) continue;
|
|
804
|
-
meta[kv[1]] = kv[2].replace(/^['"]|['"]$/g, '').trim();
|
|
805
|
-
}
|
|
806
|
-
return meta;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function readOutputStyleMetadata(filePath, source) {
|
|
810
|
-
let raw = '';
|
|
811
|
-
try { raw = readFileSync(filePath, 'utf8'); } catch { return null; }
|
|
812
|
-
const meta = parseOutputStyleFrontmatter(raw);
|
|
813
|
-
const fileId = normalizeOutputStyleId(basename(filePath).replace(/\.md$/i, ''));
|
|
814
|
-
const id = normalizeOutputStyleId(meta.name) || fileId;
|
|
815
|
-
if (!id) return null;
|
|
816
|
-
const aliases = clean(meta.aliases)
|
|
817
|
-
.split(',')
|
|
818
|
-
.map((value) => normalizeOutputStyleId(value))
|
|
819
|
-
.filter(Boolean);
|
|
820
|
-
const label = clean(meta.title || meta.label) || titleCaseOutputStyle(id);
|
|
821
|
-
return {
|
|
822
|
-
id,
|
|
823
|
-
label,
|
|
824
|
-
description: clean(meta.description),
|
|
825
|
-
aliases,
|
|
826
|
-
source,
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function listOutputStyleCatalog(dataDir = STANDALONE_DATA_DIR) {
|
|
831
|
-
const byId = new Map();
|
|
832
|
-
const dirs = [
|
|
833
|
-
{ dir: join(STANDALONE_ROOT, 'output-styles'), source: 'builtin' },
|
|
834
|
-
{ dir: join(dataDir || STANDALONE_DATA_DIR, 'output-styles'), source: 'user' },
|
|
835
|
-
];
|
|
836
|
-
for (const { dir, source } of dirs) {
|
|
837
|
-
let entries = [];
|
|
838
|
-
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
839
|
-
for (const entry of entries) {
|
|
840
|
-
if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.md')) continue;
|
|
841
|
-
const style = readOutputStyleMetadata(join(dir, entry.name), source);
|
|
842
|
-
if (style) byId.set(style.id, style);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
return [...byId.values()].sort((a, b) => {
|
|
846
|
-
const ai = OUTPUT_STYLE_ORDER.indexOf(a.id);
|
|
847
|
-
const bi = OUTPUT_STYLE_ORDER.indexOf(b.id);
|
|
848
|
-
if (ai !== bi) return (ai < 0 ? 999 : ai) - (bi < 0 ? 999 : bi);
|
|
849
|
-
return a.label.localeCompare(b.label, 'en', { sensitivity: 'base' });
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function findOutputStyle(value, styles) {
|
|
854
|
-
const id = normalizeOutputStyleId(value);
|
|
855
|
-
const compact = outputStyleCompactKey(value);
|
|
856
|
-
if (!id && !compact) return null;
|
|
857
|
-
return (styles || []).find((style) => {
|
|
858
|
-
if (style.id === id || outputStyleCompactKey(style.id) === compact) return true;
|
|
859
|
-
if (outputStyleCompactKey(style.label) === compact) return true;
|
|
860
|
-
return (style.aliases || []).some((alias) => alias === id || outputStyleCompactKey(alias) === compact);
|
|
861
|
-
}) || null;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
function configuredOutputStyleValue(dataDir = STANDALONE_DATA_DIR) {
|
|
865
|
-
const unified = readJsonSafe(join(dataDir || STANDALONE_DATA_DIR, 'mixdog-config.json')) || {};
|
|
866
|
-
return clean(unified.outputStyle || (unified.agent && unified.agent.outputStyle) || 'default') || 'default';
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
function outputStyleStatus(dataDir = STANDALONE_DATA_DIR) {
|
|
870
|
-
const styles = listOutputStyleCatalog(dataDir);
|
|
871
|
-
const configured = configuredOutputStyleValue(dataDir);
|
|
872
|
-
const current = findOutputStyle(configured, styles)
|
|
873
|
-
|| findOutputStyle('default', styles)
|
|
874
|
-
|| styles[0]
|
|
875
|
-
|| { id: 'default', label: 'Default', description: '', aliases: [], source: 'builtin' };
|
|
876
|
-
return { configured, current, styles };
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
function sessionHasConversationMessages(activeSession) {
|
|
880
|
-
const messages = Array.isArray(activeSession?.messages) ? activeSession.messages : [];
|
|
881
|
-
return messages.some((message) => {
|
|
882
|
-
const role = message?.role;
|
|
883
|
-
if (role !== 'user' && role !== 'assistant' && role !== 'tool') return false;
|
|
884
|
-
const text = sessionMessageText(message.content).trim();
|
|
885
|
-
if (!text && role !== 'assistant') return false;
|
|
886
|
-
if (role === 'user' && isSessionPreviewNoise(text)) return false;
|
|
887
|
-
return true;
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function readJsonSafe(path) {
|
|
892
|
-
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function countSkillFiles(root) {
|
|
896
|
-
const skillsDir = join(root, 'skills');
|
|
897
|
-
if (!existsSync(skillsDir)) return 0;
|
|
898
|
-
let count = 0;
|
|
899
|
-
const walk = (dir) => {
|
|
900
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
901
|
-
const full = join(dir, entry.name);
|
|
902
|
-
if (entry.isDirectory()) walk(full);
|
|
903
|
-
else if (/^(SKILL|skill)\.md$/i.test(entry.name) || entry.name.toLowerCase().endsWith('.md')) count += 1;
|
|
904
|
-
}
|
|
905
|
-
};
|
|
906
|
-
try { walk(skillsDir); } catch { return count; }
|
|
907
|
-
return count;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
function mcpScriptForPlugin(root) {
|
|
911
|
-
const candidates = [
|
|
912
|
-
'scripts/run-mcp.mjs',
|
|
913
|
-
'mcp/server.mjs',
|
|
914
|
-
'server.mjs',
|
|
915
|
-
];
|
|
916
|
-
return candidates.find((rel) => existsSync(join(root, rel))) || null;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
function pluginManifest(root) {
|
|
920
|
-
return readJsonSafe(join(root, '.codex-plugin', 'plugin.json'))
|
|
921
|
-
|| readJsonSafe(join(root, 'plugin.json'))
|
|
922
|
-
|| {};
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
function pluginMcpServerName(plugin = {}) {
|
|
926
|
-
const base = clean(plugin.name || plugin.title || 'plugin')
|
|
927
|
-
.toLowerCase()
|
|
928
|
-
.replace(/[^a-z0-9_.-]+/g, '-')
|
|
929
|
-
.replace(/^-+|-+$/g, '');
|
|
930
|
-
return base ? `plugin-${base}` : 'plugin-mcp';
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
function findPreset(config, key) {
|
|
934
|
-
const wanted = clean(key).toLowerCase();
|
|
935
|
-
if (!wanted) return null;
|
|
936
|
-
const presets = Array.isArray(config?.presets) ? config.presets : [];
|
|
937
|
-
return presets.find((p) => {
|
|
938
|
-
const id = clean(p?.id).toLowerCase();
|
|
939
|
-
const name = clean(p?.name).toLowerCase();
|
|
940
|
-
return id === wanted || name === wanted;
|
|
941
|
-
}) || null;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
function resolveRoute(config, { provider, model, effort, fast } = {}) {
|
|
945
|
-
const explicitProvider = clean(provider);
|
|
946
|
-
const explicitModel = clean(model);
|
|
947
|
-
const hasExplicitEffort = effort !== undefined;
|
|
948
|
-
const explicitEffort = hasExplicitEffort ? normalizeEffortInput(effort) : undefined;
|
|
949
|
-
const hasExplicitFast = fast !== undefined;
|
|
950
|
-
const explicitFast = fast === true;
|
|
951
|
-
|
|
952
|
-
if (explicitModel && !explicitProvider) {
|
|
953
|
-
const preset = findPreset(config, explicitModel);
|
|
954
|
-
if (preset) {
|
|
955
|
-
const p = clean(preset.provider) || DEFAULT_PROVIDER;
|
|
956
|
-
const m = clean(preset.model) || DEFAULT_MODEL;
|
|
957
|
-
const saved = modelSettingsFor(config, p, m);
|
|
958
|
-
return {
|
|
959
|
-
provider: p,
|
|
960
|
-
model: m,
|
|
961
|
-
preset,
|
|
962
|
-
effort: hasExplicitEffort ? explicitEffort : normalizeSavedEffort(saved.effort ?? preset.effort),
|
|
963
|
-
fast: hasExplicitFast ? explicitFast : (hasOwn(saved, 'fast') ? saved.fast === true : (preset.fast === true || fastPreferenceFor(config, p, m))),
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
if (!explicitProvider && !explicitModel) {
|
|
969
|
-
const defaultKey = config?.default;
|
|
970
|
-
const preset = findPreset(config, defaultKey);
|
|
971
|
-
if (preset) {
|
|
972
|
-
const p = clean(preset.provider) || DEFAULT_PROVIDER;
|
|
973
|
-
const m = clean(preset.model) || DEFAULT_MODEL;
|
|
974
|
-
const saved = modelSettingsFor(config, p, m);
|
|
975
|
-
return {
|
|
976
|
-
provider: p,
|
|
977
|
-
model: m,
|
|
978
|
-
preset,
|
|
979
|
-
effort: hasExplicitEffort ? explicitEffort : normalizeSavedEffort(saved.effort ?? preset.effort),
|
|
980
|
-
fast: hasExplicitFast ? explicitFast : (hasOwn(saved, 'fast') ? saved.fast === true : (preset.fast === true || fastPreferenceFor(config, p, m))),
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
const p = explicitProvider || DEFAULT_PROVIDER;
|
|
986
|
-
const m = explicitModel || DEFAULT_MODEL;
|
|
987
|
-
const saved = modelSettingsFor(config, p, m);
|
|
988
|
-
return {
|
|
989
|
-
provider: p,
|
|
990
|
-
model: m,
|
|
991
|
-
preset: null,
|
|
992
|
-
effort: hasExplicitEffort ? explicitEffort : normalizeSavedEffort(saved.effort),
|
|
993
|
-
fast: hasExplicitFast ? explicitFast : (hasOwn(saved, 'fast') ? saved.fast === true : fastPreferenceFor(config, p, m)),
|
|
994
|
-
};
|
|
995
|
-
}
|
|
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
|
-
|
|
1012
|
-
function ensureProviderEnabled(config, provider) {
|
|
1013
|
-
const providers = { ...(config?.providers || {}) };
|
|
1014
|
-
providers[provider] = { ...(providers[provider] || {}), enabled: true };
|
|
1015
|
-
return providers;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
const AUTO_CLEAR_DEFAULT_IDLE_MS = 60 * 60 * 1000;
|
|
1019
|
-
|
|
1020
|
-
function normalizeSystemShellConfig(value = {}) {
|
|
1021
|
-
const raw = value && typeof value === 'object' ? value : {};
|
|
1022
|
-
const command = clean(raw.command ?? raw.path ?? raw.executable ?? raw.shell);
|
|
1023
|
-
const envCommand = clean(process.env.MIXDOG_SHELL);
|
|
1024
|
-
return {
|
|
1025
|
-
command,
|
|
1026
|
-
effective: command || envCommand || '',
|
|
1027
|
-
source: command ? 'config' : (envCommand ? 'env' : 'auto'),
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
function normalizeSystemShellCommand(value) {
|
|
1032
|
-
const command = clean(value).replace(/^auto$/i, '').replace(/^['"](.+)['"]$/, '$1').trim();
|
|
1033
|
-
if (!command) return '';
|
|
1034
|
-
if (process.platform === 'win32') {
|
|
1035
|
-
const stem = command.split(/[\\/]/).pop().toLowerCase().replace(/\.exe$/, '');
|
|
1036
|
-
if (stem !== 'powershell' && stem !== 'pwsh') {
|
|
1037
|
-
throw new Error('system shell command must be powershell.exe or pwsh on Windows');
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
return command;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
function normalizeAutoClearConfig(value = {}) {
|
|
1044
|
-
const raw = value && typeof value === 'object' ? value : {};
|
|
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') : '';
|
|
1048
|
-
return {
|
|
1049
|
-
enabled: raw.enabled !== false,
|
|
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,
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
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
|
-
|
|
1091
|
-
function formatDurationMs(ms) {
|
|
1092
|
-
const value = Math.max(0, Number(ms) || 0);
|
|
1093
|
-
if (value % 3_600_000 === 0) return `${value / 3_600_000}h`;
|
|
1094
|
-
if (value % 60_000 === 0) return `${value / 60_000}m`;
|
|
1095
|
-
return `${Math.round(value / 1000)}s`;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
function parseDurationMs(input) {
|
|
1099
|
-
const text = clean(input).toLowerCase();
|
|
1100
|
-
if (!text) return null;
|
|
1101
|
-
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(text);
|
|
1102
|
-
if (!match) return null;
|
|
1103
|
-
const n = Number(match[1]);
|
|
1104
|
-
if (!Number.isFinite(n) || n <= 0) return null;
|
|
1105
|
-
const unit = match[2] || 'm';
|
|
1106
|
-
const mult = unit === 'h' ? 3_600_000 : unit === 'm' ? 60_000 : unit === 's' ? 1000 : 1;
|
|
1107
|
-
return Math.max(60_000, Math.round(n * mult));
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
const FAST_CAPABLE_PROVIDERS = new Set(['anthropic', 'anthropic-oauth', 'openai', 'openai-oauth']);
|
|
1111
|
-
const LAZY_SECRET_PROVIDERS = new Set(['openai-oauth', 'anthropic-oauth', 'grok-oauth', 'ollama', 'lmstudio']);
|
|
1112
|
-
|
|
1113
|
-
function routeFastKey(provider, model) {
|
|
1114
|
-
const p = clean(provider);
|
|
1115
|
-
const m = clean(model);
|
|
1116
|
-
return p && m ? `${p}/${m}` : '';
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
function openAiModelMetaSupportsFast(model) {
|
|
1120
|
-
const tiers = Array.isArray(model?.serviceTiers) ? model.serviceTiers : [];
|
|
1121
|
-
const speedTiers = Array.isArray(model?.additionalSpeedTiers) ? model.additionalSpeedTiers : [];
|
|
1122
|
-
if (tiers.length || speedTiers.length || model?.defaultServiceTier) {
|
|
1123
|
-
return tiers.some((tier) => tier?.id === 'priority')
|
|
1124
|
-
|| speedTiers.includes('priority')
|
|
1125
|
-
|| model?.defaultServiceTier === 'priority';
|
|
1126
|
-
}
|
|
1127
|
-
const id = clean(model?.id || model).toLowerCase();
|
|
1128
|
-
if (id.includes('mini') || id.includes('nano') || id.includes('codex')) return false;
|
|
1129
|
-
return /^gpt-5(\.|-|$)/.test(id);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
function openAiDirectModelSupportsFast(model) {
|
|
1133
|
-
const id = clean(model?.id || model);
|
|
1134
|
-
return /^gpt-5\.5(?:-\d{4}|$)/.test(id)
|
|
1135
|
-
|| /^gpt-5\.4(?:-\d{4}|$)/.test(id)
|
|
1136
|
-
|| /^gpt-5\.4-mini(?:-\d{4}|$)/.test(id);
|
|
1137
|
-
}
|
|
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
|
-
|
|
1177
|
-
function anthropicModelMetaSupportsFast(model) {
|
|
1178
|
-
const id = clean(model?.id || model).toLowerCase();
|
|
1179
|
-
return /^claude-(opus|sonnet)/.test(id);
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
function fastCapableFor(provider, model) {
|
|
1183
|
-
const p = clean(provider);
|
|
1184
|
-
if (!FAST_CAPABLE_PROVIDERS.has(p)) return false;
|
|
1185
|
-
if (p === 'openai') return openAiDirectModelSupportsFast(model);
|
|
1186
|
-
if (p === 'openai-oauth') return openAiModelMetaSupportsFast(model);
|
|
1187
|
-
if (p === 'anthropic' || p === 'anthropic-oauth') return anthropicModelMetaSupportsFast(model);
|
|
1188
|
-
return false;
|
|
1189
|
-
}
|
|
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
|
-
|
|
1201
|
-
function fastPreferenceFor(config, provider, model) {
|
|
1202
|
-
const key = routeFastKey(provider, model);
|
|
1203
|
-
if (!key) return false;
|
|
1204
|
-
const saved = config?.modelSettings?.[key];
|
|
1205
|
-
if (saved && typeof saved === 'object' && hasOwn(saved, 'fast')) return saved.fast === true;
|
|
1206
|
-
return config?.fastModels?.[key] === true;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
function saveModelSettings(cfgMod, route, { fastCapable = true, baseConfig = null } = {}) {
|
|
1210
|
-
const key = routeFastKey(route?.provider, route?.model);
|
|
1211
|
-
if (!key) return baseConfig || cfgMod.loadConfig();
|
|
1212
|
-
const nextConfig = baseConfig || cfgMod.loadConfig();
|
|
1213
|
-
const modelSettings = { ...(nextConfig.modelSettings || {}) };
|
|
1214
|
-
const nextSetting = { ...(modelSettings[key] || {}) };
|
|
1215
|
-
if (hasOwn(route, 'effort') && route.effort) nextSetting.effort = route.effort;
|
|
1216
|
-
else delete nextSetting.effort;
|
|
1217
|
-
if (fastCapable) nextSetting.fast = route.fast === true;
|
|
1218
|
-
else nextSetting.fast = false;
|
|
1219
|
-
modelSettings[key] = nextSetting;
|
|
1220
|
-
|
|
1221
|
-
// Legacy compatibility: keep fastModels true entries for old readers, but
|
|
1222
|
-
// let modelSettings.fast=false override them in new readers.
|
|
1223
|
-
const fastModels = { ...(nextConfig.fastModels || {}) };
|
|
1224
|
-
if (nextSetting.fast === true) fastModels[key] = true;
|
|
1225
|
-
else delete fastModels[key];
|
|
1226
|
-
|
|
1227
|
-
const savedConfig = { ...nextConfig, modelSettings, fastModels };
|
|
1228
|
-
cfgMod.saveConfig(savedConfig);
|
|
1229
|
-
return savedConfig;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
function routeForStatusline(route) {
|
|
1233
|
-
const out = {
|
|
1234
|
-
mode: 'fixed',
|
|
1235
|
-
defaultProvider: route.provider,
|
|
1236
|
-
defaultModel: route.model,
|
|
1237
|
-
};
|
|
1238
|
-
const preset = route.preset || {};
|
|
1239
|
-
if (preset.id) out.presetId = preset.id;
|
|
1240
|
-
if (preset.name) out.presetName = preset.name;
|
|
1241
|
-
if (preset.modelDisplay) out.modelDisplay = preset.modelDisplay;
|
|
1242
|
-
if (route.fast === true || route.fast === false) out.fast = route.fast;
|
|
1243
|
-
else if (preset.fast === true || preset.fast === false) out.fast = preset.fast;
|
|
1244
|
-
if (route.effectiveEffort) {
|
|
1245
|
-
out.effort = route.effectiveEffort;
|
|
1246
|
-
out.displayEffort = route.effectiveEffort;
|
|
1247
|
-
} else if (hasOwn(route, 'effort')) {
|
|
1248
|
-
delete out.effort;
|
|
1249
|
-
delete out.displayEffort;
|
|
1250
|
-
}
|
|
1251
|
-
return out;
|
|
1252
|
-
}
|
|
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
|
-
}
|
|
374
|
+
const applyStandaloneToolDefaults = __applyStandaloneToolDefaultsForTest;
|
|
375
|
+
const outputStyleStatus = (dataDir = STANDALONE_DATA_DIR) => outputStyleStatusRaw(STANDALONE_ROOT, dataDir || STANDALONE_DATA_DIR);
|
|
1259
376
|
|
|
1260
377
|
const ONBOARDING_VERSION = 1;
|
|
1261
|
-
const WORKFLOW_ROUTE_SLOTS = ['lead', 'agent', 'explorer', 'memory'];
|
|
1262
|
-
const FIXED_AGENT_SLOTS = Object.freeze([
|
|
1263
|
-
{ id: 'explore', label: 'Explore', description: 'Broad repository exploration', workflowSlot: 'explorer' },
|
|
1264
|
-
{ id: 'maintainer', label: 'Maintainer', description: 'Background memory and upkeep', workflowSlot: 'memory' },
|
|
1265
|
-
{ id: 'worker', label: 'Worker', description: 'Scoped implementation' },
|
|
1266
|
-
{ id: 'heavy-worker', label: 'Heavy Worker', description: 'Broad or multi-file implementation' },
|
|
1267
|
-
{ id: 'reviewer', label: 'Reviewer', description: 'Diff review and risk checks' },
|
|
1268
|
-
{ id: 'debugger', label: 'Debugger', description: 'Root-cause analysis and failure tracing' },
|
|
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
378
|
const QUICK_SEARCH_MODELS = Object.freeze({
|
|
1288
379
|
'openai-oauth': [
|
|
1289
380
|
{ id: 'gpt-5.5', display: 'GPT-5.5', latest: true, contextWindow: 1000000 },
|
|
@@ -1314,851 +405,37 @@ const QUICK_SEARCH_MODELS = Object.freeze({
|
|
|
1314
405
|
{ id: 'gemini-2.5-flash', display: 'Gemini 2.5 Flash', contextWindow: 1000000 },
|
|
1315
406
|
{ id: 'gemini-2.0-flash', display: 'Gemini 2.0 Flash', contextWindow: 1000000 },
|
|
1316
407
|
],
|
|
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
|
-
});
|
|
1328
|
-
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
if (id === 'heavy' || id === 'heavyworker') return 'heavy-worker';
|
|
1349
|
-
if (id === 'review') return 'reviewer';
|
|
1350
|
-
if (id === 'debug') return 'debugger';
|
|
1351
|
-
return AGENT_ROLE_IDS.has(id) ? id : '';
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
function normalizeWorkflowId(value, fallback = '') {
|
|
1355
|
-
const id = clean(value).toLowerCase().replace(/[\s_]+/g, '-');
|
|
1356
|
-
return /^[a-z0-9][a-z0-9_.-]*$/.test(id) ? id : fallback;
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
function readTextSafe(path) {
|
|
1360
|
-
try { return readFileSync(path, 'utf8').trim(); } catch { return ''; }
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
function workflowSourceDirs(dataDir) {
|
|
1364
|
-
return [
|
|
1365
|
-
{ root: join(STANDALONE_ROOT, 'workflows'), source: 'built-in' },
|
|
1366
|
-
{ root: join(dataDir || STANDALONE_DATA_DIR, 'workflows'), source: 'user' },
|
|
1367
|
-
];
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
function agentSourceDirs(dataDir, id) {
|
|
1371
|
-
return [
|
|
1372
|
-
join(dataDir || STANDALONE_DATA_DIR, 'agents', id),
|
|
1373
|
-
join(STANDALONE_ROOT, 'agents', id),
|
|
1374
|
-
];
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
function readWorkflowPackFromDir(dir, source = 'built-in') {
|
|
1378
|
-
const manifest = readJsonSafe(join(dir, 'workflow.json'));
|
|
1379
|
-
if (!manifest || typeof manifest !== 'object') return null;
|
|
1380
|
-
const id = normalizeWorkflowId(manifest.id || manifest.name);
|
|
1381
|
-
if (!id) return null;
|
|
1382
|
-
const entry = clean(manifest.entry) || 'WORKFLOW.md';
|
|
1383
|
-
const body = readTextSafe(join(dir, entry));
|
|
1384
|
-
if (!body) return null;
|
|
1385
|
-
const agentsConfigured = Array.isArray(manifest.agents);
|
|
1386
|
-
return {
|
|
1387
|
-
id,
|
|
1388
|
-
name: clean(manifest.name) || id,
|
|
1389
|
-
description: clean(manifest.description),
|
|
1390
|
-
entry,
|
|
1391
|
-
agentsConfigured,
|
|
1392
|
-
agents: agentsConfigured ? manifest.agents.map((agent) => normalizeAgentId(agent) || normalizeWorkflowId(agent)).filter(Boolean) : [],
|
|
1393
|
-
body,
|
|
1394
|
-
source,
|
|
1395
|
-
};
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
function listWorkflowPacks(dataDir) {
|
|
1399
|
-
const byId = new Map();
|
|
1400
|
-
for (const { root, source } of workflowSourceDirs(dataDir)) {
|
|
1401
|
-
if (!existsSync(root)) continue;
|
|
1402
|
-
let entries = [];
|
|
1403
|
-
try { entries = readdirSync(root, { withFileTypes: true }); } catch { entries = []; }
|
|
1404
|
-
for (const entry of entries) {
|
|
1405
|
-
if (!entry.isDirectory()) continue;
|
|
1406
|
-
const pack = readWorkflowPackFromDir(join(root, entry.name), source);
|
|
1407
|
-
if (pack) byId.set(pack.id, pack);
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
return [...byId.values()].sort((a, b) => {
|
|
1411
|
-
if (a.id === DEFAULT_WORKFLOW_ID) return -1;
|
|
1412
|
-
if (b.id === DEFAULT_WORKFLOW_ID) return 1;
|
|
1413
|
-
return a.name.localeCompare(b.name);
|
|
1414
|
-
});
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
function activeWorkflowId(config) {
|
|
1418
|
-
return normalizeWorkflowId(config?.workflow?.active, DEFAULT_WORKFLOW_ID);
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
function loadWorkflowPack(dataDir, id) {
|
|
1422
|
-
const wanted = normalizeWorkflowId(id, DEFAULT_WORKFLOW_ID);
|
|
1423
|
-
for (const { root, source } of workflowSourceDirs(dataDir).reverse()) {
|
|
1424
|
-
const pack = readWorkflowPackFromDir(join(root, wanted), source);
|
|
1425
|
-
if (pack) return pack;
|
|
1426
|
-
}
|
|
1427
|
-
return readWorkflowPackFromDir(join(STANDALONE_ROOT, 'workflows', DEFAULT_WORKFLOW_ID), 'built-in');
|
|
1428
|
-
}
|
|
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
|
-
|
|
1444
|
-
function loadAgentDefinition(dataDir, id) {
|
|
1445
|
-
const agentId = normalizeAgentId(id) || normalizeWorkflowId(id);
|
|
1446
|
-
if (!agentId) return null;
|
|
1447
|
-
const cacheKey = `${dataDir || STANDALONE_DATA_DIR}\n${agentId}`;
|
|
1448
|
-
if (agentDefinitionCache.has(cacheKey)) return agentDefinitionCache.get(cacheKey);
|
|
1449
|
-
for (const dir of agentSourceDirs(dataDir, agentId)) {
|
|
1450
|
-
const manifest = readJsonSafe(join(dir, 'agent.json')) || {};
|
|
1451
|
-
const entry = clean(manifest.entry) || 'AGENT.md';
|
|
1452
|
-
const doc = readMarkdownDocument(readTextSafe(join(dir, entry)));
|
|
1453
|
-
const body = doc.body;
|
|
1454
|
-
if (!body) continue;
|
|
1455
|
-
const definition = {
|
|
1456
|
-
id: agentId,
|
|
1457
|
-
name: clean(manifest.name) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
|
|
1458
|
-
description: clean(manifest.description) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.description || '',
|
|
1459
|
-
permission: normalizeAgentPermissionOrNone(doc.frontmatter.permission),
|
|
1460
|
-
frontmatter: doc.frontmatter,
|
|
1461
|
-
body,
|
|
1462
|
-
};
|
|
1463
|
-
agentDefinitionCache.set(cacheKey, definition);
|
|
1464
|
-
return definition;
|
|
1465
|
-
}
|
|
1466
|
-
const legacyDoc = readMarkdownDocument(readTextSafe(join(STANDALONE_ROOT, 'agents', `${agentId}.md`)));
|
|
1467
|
-
if (!legacyDoc.body) {
|
|
1468
|
-
agentDefinitionCache.set(cacheKey, null);
|
|
1469
|
-
return null;
|
|
1470
|
-
}
|
|
1471
|
-
const definition = {
|
|
1472
|
-
id: agentId,
|
|
1473
|
-
name: FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
|
|
1474
|
-
description: '',
|
|
1475
|
-
permission: normalizeAgentPermissionOrNone(legacyDoc.frontmatter.permission),
|
|
1476
|
-
frontmatter: legacyDoc.frontmatter,
|
|
1477
|
-
body: legacyDoc.body,
|
|
1478
|
-
};
|
|
1479
|
-
agentDefinitionCache.set(cacheKey, definition);
|
|
1480
|
-
return definition;
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
function workflowContextBlock(config, dataDir) {
|
|
1484
|
-
const pack = loadWorkflowPack(dataDir, activeWorkflowId(config));
|
|
1485
|
-
if (!pack) return '';
|
|
1486
|
-
const lines = [
|
|
1487
|
-
`# Active Workflow: ${pack.name}`,
|
|
1488
|
-
];
|
|
1489
|
-
if (pack.description) lines.push(pack.description);
|
|
1490
|
-
lines.push(pack.body);
|
|
1491
|
-
|
|
1492
|
-
const agentIds = pack.agentsConfigured ? pack.agents : FIXED_AGENT_SLOTS.map((agent) => agent.id);
|
|
1493
|
-
const agentBlocks = agentIds
|
|
1494
|
-
.map((id) => loadAgentDefinition(dataDir, id))
|
|
1495
|
-
.filter(Boolean);
|
|
1496
|
-
if (agentBlocks.length) {
|
|
1497
|
-
lines.push('# Available Agents');
|
|
1498
|
-
for (const agent of agentBlocks) {
|
|
1499
|
-
lines.push(`## ${agent.name} (${agent.id})`);
|
|
1500
|
-
if (agent.description) lines.push(agent.description);
|
|
1501
|
-
lines.push(agent.body);
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
return lines.join('\n\n');
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
function normalizeWorkflowRoute(routeLike, fallback = {}) {
|
|
1508
|
-
const provider = clean(routeLike?.provider) || clean(fallback.provider);
|
|
1509
|
-
const model = clean(routeLike?.model) || clean(fallback.model);
|
|
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;
|
|
1516
|
-
const effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
|
|
1517
|
-
const fast = routeLike?.fast ?? fallback.fast;
|
|
1518
|
-
return {
|
|
1519
|
-
provider,
|
|
1520
|
-
model,
|
|
1521
|
-
...(effort ? { effort } : {}),
|
|
1522
|
-
...(fast === true ? { fast: true } : {}),
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
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
|
-
|
|
1561
|
-
function upsertWorkflowPreset(presets, slot, routeLike) {
|
|
1562
|
-
const route = normalizeWorkflowRoute(routeLike);
|
|
1563
|
-
if (!route) return presets;
|
|
1564
|
-
const id = workflowPresetId(slot);
|
|
1565
|
-
const preset = {
|
|
1566
|
-
id,
|
|
1567
|
-
name: workflowPresetName(slot),
|
|
1568
|
-
type: 'agent',
|
|
1569
|
-
provider: route.provider,
|
|
1570
|
-
model: route.model,
|
|
1571
|
-
...(route.effort ? { effort: route.effort } : {}),
|
|
1572
|
-
...(route.fast === true ? { fast: true } : {}),
|
|
1573
|
-
tools: 'full',
|
|
1574
|
-
};
|
|
1575
|
-
const next = (Array.isArray(presets) ? presets : []).filter((p) => clean(p?.id) !== id && clean(p?.name) !== preset.name);
|
|
1576
|
-
next.push(preset);
|
|
1577
|
-
return next;
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
function summarizeWorkflowRoutes(config) {
|
|
1581
|
-
const routes = config?.workflowRoutes && typeof config.workflowRoutes === 'object' ? config.workflowRoutes : {};
|
|
1582
|
-
const out = {};
|
|
1583
|
-
for (const slot of WORKFLOW_ROUTE_SLOTS) {
|
|
1584
|
-
const route = routes[slot];
|
|
1585
|
-
if (route?.provider && route?.model) out[slot] = normalizeWorkflowRoute(route);
|
|
1586
|
-
}
|
|
1587
|
-
return out;
|
|
1588
|
-
}
|
|
1589
|
-
|
|
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;
|
|
1597
|
-
}
|
|
1598
|
-
const preset = findPreset(config, slotValue);
|
|
1599
|
-
return preset ? normalizeWorkflowRoute(preset) : null;
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
function agentRouteFromConfig(config, agentId, _dataDir) {
|
|
1603
|
-
const id = normalizeAgentId(agentId);
|
|
1604
|
-
if (!id) return null;
|
|
1605
|
-
const explicit = normalizeWorkflowRoute(config?.agents?.[id])
|
|
1606
|
-
|| (id === 'maintainer' ? normalizeWorkflowRoute(config?.agents?.maintenance) : null);
|
|
1607
|
-
if (explicit) return explicit;
|
|
1608
|
-
|
|
1609
|
-
const agent = FIXED_AGENT_SLOTS.find((item) => item.id === id);
|
|
1610
|
-
if (agent?.workflowSlot) {
|
|
1611
|
-
const workflowRoute = normalizeWorkflowRoute(config?.workflowRoutes?.[agent.workflowSlot]);
|
|
1612
|
-
if (workflowRoute) return workflowRoute;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
if (id === 'explore') return routeFromPreset(config, config?.maintenance?.explore);
|
|
1616
|
-
if (id === 'maintainer') return routeFromPreset(config, config?.maintenance?.memory);
|
|
1617
|
-
|
|
1618
|
-
return null;
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
function toolResponseText(result) {
|
|
1622
|
-
if (result && typeof result === 'object' && Array.isArray(result.content)) {
|
|
1623
|
-
return result.content
|
|
1624
|
-
.map((part) => (part?.type === 'text' ? part.text || '' : JSON.stringify(part)))
|
|
1625
|
-
.join('\n');
|
|
1626
|
-
}
|
|
1627
|
-
if (typeof result === 'string') return result;
|
|
1628
|
-
return JSON.stringify(result, null, 2);
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
function isEmptyRecallText(value) {
|
|
1632
|
-
const text = String(value || '').trim();
|
|
1633
|
-
return !text || /^\(?no results\)?$/i.test(text) || /^\(?empty memory result\)?$/i.test(text);
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
function currentSessionRecallRows(session, query, { limit = 10 } = {}) {
|
|
1637
|
-
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
1638
|
-
if (!messages.length) return '(no results)';
|
|
1639
|
-
const terms = [...new Set(String(query || '').toLowerCase().match(/[\p{L}\p{N}_./:-]{2,}/gu) || [])]
|
|
1640
|
-
.filter(Boolean)
|
|
1641
|
-
.slice(0, 16);
|
|
1642
|
-
const max = Math.max(1, Math.min(100, Number(limit) || 10));
|
|
1643
|
-
const rows = [];
|
|
1644
|
-
for (let i = messages.length - 1; i >= 0 && rows.length < max; i -= 1) {
|
|
1645
|
-
const m = messages[i];
|
|
1646
|
-
if (!m || (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'tool')) continue;
|
|
1647
|
-
const text = messageContextText(m).replace(/\s+/g, ' ').trim();
|
|
1648
|
-
if (!text) continue;
|
|
1649
|
-
if (terms.length && !terms.some((term) => text.toLowerCase().includes(term))) continue;
|
|
1650
|
-
rows.push(`[session:${i + 1}] ${m.role}: ${text.slice(0, 1000)}`);
|
|
1651
|
-
}
|
|
1652
|
-
return rows.length ? rows.join('\n') : '(no results)';
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
function parseToolSelection(value) {
|
|
1656
|
-
if (Array.isArray(value)) return value.map(clean).filter(Boolean);
|
|
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, '')
|
|
1661
|
-
.split(/[,\s]+/)
|
|
1662
|
-
.map(clean)
|
|
1663
|
-
.filter(Boolean);
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
function parseToolSearchQuerySelection(query) {
|
|
1667
|
-
const match = clean(query).match(/^select\s*:\s*(.+)$/i);
|
|
1668
|
-
return match ? parseToolSelection(match[1]) : [];
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
function toolKind(tool) {
|
|
1672
|
-
const name = clean(tool?.name);
|
|
1673
|
-
if (name.startsWith('mcp__')) return 'mcp';
|
|
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';
|
|
1678
|
-
return 'tool';
|
|
1679
|
-
}
|
|
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
|
-
|
|
1708
|
-
function measuredToolUsage(name) {
|
|
1709
|
-
return MEASURED_TOOL_USAGE[clean(name)] || 0;
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
function measuredToolRank(name) {
|
|
1713
|
-
const index = MEASURED_TOOL_ORDER.indexOf(clean(name));
|
|
1714
|
-
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
function sortedCatalogByMeasuredUsage(catalog) {
|
|
1718
|
-
return (catalog || [])
|
|
1719
|
-
.map((tool, index) => ({ tool, index }))
|
|
1720
|
-
.sort((a, b) => {
|
|
1721
|
-
const au = measuredToolUsage(a.tool?.name);
|
|
1722
|
-
const bu = measuredToolUsage(b.tool?.name);
|
|
1723
|
-
if (bu !== au) return bu - au;
|
|
1724
|
-
const ar = measuredToolRank(a.tool?.name);
|
|
1725
|
-
const br = measuredToolRank(b.tool?.name);
|
|
1726
|
-
if (ar !== br) return ar - br;
|
|
1727
|
-
return a.index - b.index;
|
|
1728
|
-
})
|
|
1729
|
-
.map((entry) => entry.tool);
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
function activeToolForSurface(tool) {
|
|
1733
|
-
if (!tool || typeof tool !== 'object') return tool;
|
|
1734
|
-
return JSON.parse(JSON.stringify(tool));
|
|
1735
|
-
}
|
|
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
|
-
|
|
1748
|
-
function filterDisallowedTools(tools, disallowed = []) {
|
|
1749
|
-
if (!Array.isArray(disallowed) || disallowed.length === 0) return tools;
|
|
1750
|
-
const deny = new Set(disallowed.map((name) => clean(name)).filter(Boolean));
|
|
1751
|
-
if (deny.size === 0) return tools;
|
|
1752
|
-
return (tools || []).filter((tool) => !deny.has(clean(tool?.name)));
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
function sortedNamesByMeasuredUsage(names) {
|
|
1756
|
-
return [...(names || [])].sort((a, b) => {
|
|
1757
|
-
const au = measuredToolUsage(a);
|
|
1758
|
-
const bu = measuredToolUsage(b);
|
|
1759
|
-
if (bu !== au) return bu - au;
|
|
1760
|
-
const ar = measuredToolRank(a);
|
|
1761
|
-
const br = measuredToolRank(b);
|
|
1762
|
-
if (ar !== br) return ar - br;
|
|
1763
|
-
return String(a).localeCompare(String(b));
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
export function defaultDeferredToolNames(catalog, mode) {
|
|
1768
|
-
const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
1769
|
-
if (mode === 'lead') {
|
|
1770
|
-
return new Set(DEFERRED_DEFAULT_LEAD_TOOLS.filter((name) => available.has(name)));
|
|
1771
|
-
}
|
|
1772
|
-
if (mode === 'readonly') {
|
|
1773
|
-
return new Set(DEFERRED_DEFAULT_READONLY_TOOLS.filter((name) => available.has(name)));
|
|
1774
|
-
}
|
|
1775
|
-
return new Set(DEFERRED_DEFAULT_FULL_TOOLS.filter((name) => available.has(name)));
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
export function compactToolSearchDescription(value, max = 220) {
|
|
1779
|
-
const text = clean(value).replace(/\s+/g, ' ');
|
|
1780
|
-
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
function toolRow(tool, activeNames = new Set()) {
|
|
1784
|
-
const name = clean(tool?.name);
|
|
1785
|
-
return {
|
|
1786
|
-
name,
|
|
1787
|
-
kind: toolKind(tool),
|
|
1788
|
-
usage: measuredToolUsage(name),
|
|
1789
|
-
active: activeNames.has(name),
|
|
1790
|
-
description: compactToolSearchDescription(tool?.description),
|
|
1791
|
-
};
|
|
1792
|
-
}
|
|
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
|
-
|
|
1832
|
-
function toolSearchTokens(value) {
|
|
1833
|
-
return (clean(value).toLowerCase().match(/[a-z0-9_.-]+/g) || [])
|
|
1834
|
-
.map((token) => token.replace(/[-.]+/g, '_'))
|
|
1835
|
-
.filter(Boolean);
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
function toolSearchMeaningfulTokens(value) {
|
|
1839
|
-
return toolSearchTokens(value).filter((token) => !TOOL_SEARCH_STOP_WORDS.has(token));
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
function toolSearchText(row) {
|
|
1843
|
-
const text = `${row.name} ${String(row.name || '').replace(/_/g, ' ')} ${row.kind} ${row.description} ${row.active ? 'active' : 'deferred'}`;
|
|
1844
|
-
return `${text} ${text.replace(/[-.]+/g, '_')}`.toLowerCase();
|
|
1845
|
-
}
|
|
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
|
-
|
|
1910
|
-
function toolSearchMatches(row, query) {
|
|
1911
|
-
const raw = clean(query).toLowerCase();
|
|
1912
|
-
if (!raw) return true;
|
|
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 [];
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
function expandSelectionNames(names) {
|
|
1971
|
-
const out = [];
|
|
1972
|
-
for (const raw of names || []) {
|
|
1973
|
-
const key = clean(raw);
|
|
1974
|
-
if (!key) continue;
|
|
1975
|
-
const alias = DEFERRED_SELECT_ALIASES[key.toLowerCase()];
|
|
1976
|
-
if (alias) out.push(...alias);
|
|
1977
|
-
else out.push(key);
|
|
1978
|
-
}
|
|
1979
|
-
return [...new Set(out)];
|
|
1980
|
-
}
|
|
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
|
-
|
|
2015
|
-
function isReadonlySelectable(tool) {
|
|
2016
|
-
const name = clean(tool?.name);
|
|
2017
|
-
if (READONLY_TOOL_NAMES.has(name)) return true;
|
|
2018
|
-
const annotations = tool?.annotations || {};
|
|
2019
|
-
if (annotations.destructiveHint === true) return false;
|
|
2020
|
-
if (annotations.readOnlyHint === true) return true;
|
|
2021
|
-
return false;
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
function applyDeferredToolSurface(session, mode, extraTools = [], options = {}) {
|
|
2025
|
-
if (!session || !Array.isArray(session.tools)) return session;
|
|
2026
|
-
const providerMode = deferredProviderMode(options.provider || session.provider);
|
|
2027
|
-
const byName = new Map();
|
|
2028
|
-
for (const tool of [...session.tools, ...(extraTools || [])]) {
|
|
2029
|
-
const name = clean(tool?.name);
|
|
2030
|
-
if (!name || byName.has(name)) continue;
|
|
2031
|
-
byName.set(name, activeToolForSurface(tool));
|
|
2032
|
-
}
|
|
2033
|
-
const catalog = sortedCatalogByMeasuredUsage([...byName.values()]);
|
|
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);
|
|
2044
|
-
session.deferredToolCatalog = catalog;
|
|
2045
|
-
session.deferredToolUsage = MEASURED_TOOL_USAGE;
|
|
2046
|
-
session.deferredDefaultTools = sortedNamesByMeasuredUsage(defaultNames);
|
|
2047
|
-
session.deferredProviderMode = providerMode;
|
|
2048
|
-
session.deferredNativeTools = providerMode === 'native';
|
|
2049
|
-
session.tools.length = 0;
|
|
2050
|
-
const active = [];
|
|
2051
|
-
for (const tool of catalog) {
|
|
2052
|
-
if (!selected.has(clean(tool?.name))) continue;
|
|
2053
|
-
if (mode === 'readonly' && !isReadonlySelectable(tool)) continue;
|
|
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);
|
|
2063
|
-
}
|
|
2064
|
-
return session;
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
function selectDeferredTools(session, names, mode) {
|
|
2068
|
-
const catalog = Array.isArray(session?.deferredToolCatalog)
|
|
2069
|
-
? session.deferredToolCatalog
|
|
2070
|
-
: (Array.isArray(session?.tools) ? session.tools : []);
|
|
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
|
-
}
|
|
2081
|
-
const added = [];
|
|
2082
|
-
const already = [];
|
|
2083
|
-
const blocked = [];
|
|
2084
|
-
const missing = [];
|
|
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);
|
|
2089
|
-
if (!tool) {
|
|
2090
|
-
missing.push(requestedName);
|
|
2091
|
-
continue;
|
|
2092
|
-
}
|
|
2093
|
-
if (mode === 'readonly' && !isReadonlySelectable(tool)) {
|
|
2094
|
-
blocked.push({ name, reason: 'readonly mode' });
|
|
2095
|
-
continue;
|
|
2096
|
-
}
|
|
2097
|
-
if (active.has(name) || discovered.has(name)) {
|
|
2098
|
-
already.push(name);
|
|
2099
|
-
continue;
|
|
2100
|
-
}
|
|
2101
|
-
if (native) {
|
|
2102
|
-
discovered.add(name);
|
|
2103
|
-
} else {
|
|
2104
|
-
session.tools.push(tool);
|
|
2105
|
-
active.add(name);
|
|
2106
|
-
}
|
|
2107
|
-
added.push(name);
|
|
2108
|
-
}
|
|
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 };
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
function renderToolSearch(args = {}, session, mode = 'full') {
|
|
2119
|
-
const catalog = Array.isArray(session?.deferredToolCatalog)
|
|
2120
|
-
? session.deferredToolCatalog
|
|
2121
|
-
: (Array.isArray(session?.tools) ? session.tools : []);
|
|
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();
|
|
2127
|
-
const limit = Math.max(1, Math.min(50, Number(args.limit) || 20));
|
|
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);
|
|
2140
|
-
const matches = query
|
|
2141
|
-
? rankedToolSearchRows(rows, query)
|
|
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;
|
|
2152
|
-
return JSON.stringify({
|
|
2153
|
-
selected,
|
|
2154
|
-
...(nativeToolSearch ? { nativeToolSearch } : {}),
|
|
2155
|
-
totalMatches: matches.length,
|
|
2156
|
-
matches: matches.slice(0, limit),
|
|
2157
|
-
activeTools: sortedNamesByMeasuredUsage(nextActiveNames),
|
|
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.',
|
|
2160
|
-
}, null, 2);
|
|
2161
|
-
}
|
|
408
|
+
'anthropic-oauth': [
|
|
409
|
+
{ id: 'claude-opus-4-8', display: 'Claude Opus 4.8', latest: true, contextWindow: 1000000 },
|
|
410
|
+
{ id: 'claude-sonnet-4-6', display: 'Claude Sonnet 4.6', latest: true, contextWindow: 1000000 },
|
|
411
|
+
{ id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5', contextWindow: 200000 },
|
|
412
|
+
],
|
|
413
|
+
anthropic: [
|
|
414
|
+
{ id: 'claude-opus-4-8', display: 'Claude Opus 4.8', latest: true, contextWindow: 1000000 },
|
|
415
|
+
{ id: 'claude-sonnet-4-6', display: 'Claude Sonnet 4.6', latest: true, contextWindow: 1000000 },
|
|
416
|
+
{ id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5', contextWindow: 200000 },
|
|
417
|
+
],
|
|
418
|
+
});
|
|
419
|
+
// Workflow/agent pack loaders bound to this runtime's root/data layout.
|
|
420
|
+
const {
|
|
421
|
+
listWorkflowPacks,
|
|
422
|
+
activeWorkflowId,
|
|
423
|
+
loadWorkflowPack,
|
|
424
|
+
workflowSummary,
|
|
425
|
+
activeWorkflowSummary,
|
|
426
|
+
loadAgentDefinition,
|
|
427
|
+
workflowContextBlock,
|
|
428
|
+
} = createWorkflowHelpers({
|
|
429
|
+
rootDir: STANDALONE_ROOT,
|
|
430
|
+
dataDir: STANDALONE_DATA_DIR,
|
|
431
|
+
readMarkdownDocument,
|
|
432
|
+
normalizeAgentPermissionOrNone,
|
|
433
|
+
});
|
|
434
|
+
const {
|
|
435
|
+
summarizeWorkflowRoutes,
|
|
436
|
+
routeFromPreset,
|
|
437
|
+
agentRouteFromConfig,
|
|
438
|
+
} = createWorkflowRouteHelpers({ resolveDefaultProvider, findPreset });
|
|
2162
439
|
|
|
2163
440
|
export function __renderToolSearchForTest(args = {}, session = {}, mode = 'full') {
|
|
2164
441
|
return renderToolSearch(args, session, mode);
|
|
@@ -2182,8 +459,17 @@ export async function createMixdogSessionRuntime({
|
|
|
2182
459
|
model,
|
|
2183
460
|
cwd = process.cwd(),
|
|
2184
461
|
toolMode = 'full',
|
|
462
|
+
remote = false,
|
|
2185
463
|
} = {}) {
|
|
2186
464
|
bootProfile('session-runtime:start', { provider, model, toolMode, cwd });
|
|
465
|
+
let remoteEnabled = remote === true;
|
|
466
|
+
// Remote-mode transcript writer (Discord outbound). Lazily created per
|
|
467
|
+
// session.id + cwd inside ask(); only active while remoteEnabled.
|
|
468
|
+
let _transcriptWriter = null;
|
|
469
|
+
let _twKey = '';
|
|
470
|
+
// Last assistant text handed to the transcript writer (via onAssistantText),
|
|
471
|
+
// so the post-turn final-content append can skip an exact duplicate.
|
|
472
|
+
let _lastAppendedAssistant = '';
|
|
2187
473
|
process.env.MIXDOG_QUIET_SESSION_LOG ??= '1';
|
|
2188
474
|
const standaloneStartedAt = performance.now();
|
|
2189
475
|
ensureStandaloneEnvironment({
|
|
@@ -2590,9 +876,11 @@ export async function createMixdogSessionRuntime({
|
|
|
2590
876
|
let sessionNeedsCwdRefresh = false;
|
|
2591
877
|
let closeRequested = false;
|
|
2592
878
|
let channelStartTimer = null;
|
|
879
|
+
let channelStartPromise = null;
|
|
2593
880
|
let providerSetupWarmupTimer = null;
|
|
2594
881
|
let providerWarmupTimer = null;
|
|
2595
882
|
let providerModelWarmupTimer = null;
|
|
883
|
+
let modelCatalogWarmupTimer = null;
|
|
2596
884
|
let codeGraphPrewarmTimer = null;
|
|
2597
885
|
let codeGraphPrewarmInFlight = false;
|
|
2598
886
|
let codeGraphPrewarmQueuedCwd = '';
|
|
@@ -2622,6 +910,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2622
910
|
}
|
|
2623
911
|
const sessionPrewarmDelayMs = envDelayMs('MIXDOG_SESSION_PREWARM_DELAY_MS', 50, { min: 0, max: 10_000 });
|
|
2624
912
|
const providerSetupWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_SETUP_WARMUP_DELAY_MS', 300, { min: 0, max: 60_000 });
|
|
913
|
+
const modelCatalogWarmupDelayMs = envDelayMs('MIXDOG_MODEL_CATALOG_WARMUP_DELAY_MS', 200, { min: 0, max: 60_000 });
|
|
2625
914
|
const providerWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_WARMUP_DELAY_MS', 1_500, { min: 0, max: 60_000 });
|
|
2626
915
|
// Background model-catalog prefetch delay. Kept short so the first `/model`
|
|
2627
916
|
// open finds a warm cache instead of paying a cold full network load. The
|
|
@@ -2655,6 +944,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2655
944
|
const modelPrefetchEnabled = !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
|
|
2656
945
|
&& !envFlag('MIXDOG_DISABLE_MODEL_PREFETCH');
|
|
2657
946
|
const codeGraphPrewarmEnabled = !envFlag('MIXDOG_DISABLE_CODE_GRAPH_PREWARM');
|
|
947
|
+
const modelCatalogWarmupEnabled = !envFlag('MIXDOG_DISABLE_MODEL_CATALOG_WARMUP');
|
|
2658
948
|
// Lazy code-graph prewarm (default ON): do NOT prewarm at startup / on cwd
|
|
2659
949
|
// change — that fired ~250ms after the first frame and, in a large tree,
|
|
2660
950
|
// burned a worker (and felt like a freeze) before the user did anything.
|
|
@@ -2667,8 +957,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2667
957
|
const notificationListeners = new Set();
|
|
2668
958
|
let providerModelsCache = { models: null, at: 0 };
|
|
2669
959
|
let providerModelsPromise = null;
|
|
960
|
+
let providerModelsLoadSeq = 0;
|
|
2670
961
|
let searchProviderModelsCache = { models: null, at: 0 };
|
|
2671
|
-
let searchProviderModelsPromise = null;
|
|
2672
962
|
let usageDashboardCache = { dashboard: null, at: 0 };
|
|
2673
963
|
let usageDashboardPromise = null;
|
|
2674
964
|
let providerSetupCache = { setup: null, at: 0 };
|
|
@@ -2676,6 +966,9 @@ export async function createMixdogSessionRuntime({
|
|
|
2676
966
|
let providerSetupPromise = null;
|
|
2677
967
|
let providerInitPromise = null;
|
|
2678
968
|
let mcpFailures = [];
|
|
969
|
+
let lastProjectMcpKey = null;
|
|
970
|
+
let mcpConnectGeneration = 0;
|
|
971
|
+
let mcpConnectInFlight = null;
|
|
2679
972
|
let preSessionToolSurface = null;
|
|
2680
973
|
let contextStatusCacheKey = null;
|
|
2681
974
|
let contextStatusCacheValue = null;
|
|
@@ -2684,11 +977,87 @@ export async function createMixdogSessionRuntime({
|
|
|
2684
977
|
hooks.emit('runtime:start', { cwd: currentCwd, provider: route.provider, model: route.model, toolMode: mode });
|
|
2685
978
|
bootProfile('hooks:ready', { ms: (performance.now() - hooksStartedAt).toFixed(1) });
|
|
2686
979
|
|
|
980
|
+
// ---------------------------------------------------------------------
|
|
981
|
+
// Self-update (npm registry version check + optional auto-install).
|
|
982
|
+
// updateCheckState mirrors the last-known checkLatestVersion() result;
|
|
983
|
+
// updateProcessState tracks the in-flight install lifecycle so the TUI can
|
|
984
|
+
// poll getUpdateStatus() instead of needing a push/event channel. Both are
|
|
985
|
+
// purely in-memory (per runtime instance) — the on-disk cache lives inside
|
|
986
|
+
// update-checker.mjs and is what actually enforces the 24h TTL.
|
|
987
|
+
let updateCheckState = {
|
|
988
|
+
currentVersion: null,
|
|
989
|
+
latestVersion: null,
|
|
990
|
+
updateAvailable: false,
|
|
991
|
+
lastCheckedAt: 0,
|
|
992
|
+
};
|
|
993
|
+
// phase: 'idle' | 'checking' | 'installing' | 'installed' | 'failed'
|
|
994
|
+
let updateProcessState = { phase: 'idle', version: null, error: null };
|
|
995
|
+
|
|
996
|
+
function autoUpdateEnabled() {
|
|
997
|
+
return config?.update?.auto === true;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async function checkForUpdateInternal({ force = false } = {}) {
|
|
1001
|
+
if (updateProcessState.phase !== 'installing') updateProcessState.phase = 'checking';
|
|
1002
|
+
try {
|
|
1003
|
+
const result = await checkLatestVersion({ force, dataDir: cfgMod.getPluginData?.() || STANDALONE_DATA_DIR });
|
|
1004
|
+
updateCheckState = {
|
|
1005
|
+
currentVersion: result.currentVersion,
|
|
1006
|
+
latestVersion: result.latestVersion,
|
|
1007
|
+
updateAvailable: result.updateAvailable,
|
|
1008
|
+
lastCheckedAt: result.lastCheckedAt,
|
|
1009
|
+
};
|
|
1010
|
+
} catch {
|
|
1011
|
+
// checkLatestVersion() is already silent-safe; this catch is belt-and-
|
|
1012
|
+
// braces so a boot-time call can never crash the runtime.
|
|
1013
|
+
} finally {
|
|
1014
|
+
if (updateProcessState.phase === 'checking') updateProcessState.phase = 'idle';
|
|
1015
|
+
}
|
|
1016
|
+
return updateCheckState;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async function runUpdateNowInternal() {
|
|
1020
|
+
if (updateProcessState.phase === 'installing') {
|
|
1021
|
+
return { ...updateProcessState, alreadyInstalling: true, error: 'update already in progress' };
|
|
1022
|
+
}
|
|
1023
|
+
updateProcessState = { phase: 'installing', version: null, error: null };
|
|
1024
|
+
try {
|
|
1025
|
+
const result = await runGlobalUpdate();
|
|
1026
|
+
if (result?.ok) {
|
|
1027
|
+
updateProcessState = { phase: 'installed', version: result.version || null, error: null };
|
|
1028
|
+
} else {
|
|
1029
|
+
updateProcessState = { phase: 'failed', version: null, error: result?.error || 'update failed' };
|
|
1030
|
+
}
|
|
1031
|
+
} catch (err) {
|
|
1032
|
+
updateProcessState = { phase: 'failed', version: null, error: err?.message || String(err) };
|
|
1033
|
+
}
|
|
1034
|
+
return updateProcessState;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Non-blocking boot hook: fires after the runtime object below is fully
|
|
1038
|
+
// constructed (setTimeout(0) defers past the synchronous return), so a
|
|
1039
|
+
// slow/hanging registry request or npm install can never delay session
|
|
1040
|
+
// boot. Auto-update defaults to OFF (config.update.auto is undefined until
|
|
1041
|
+
// the user opts in via setAutoUpdate(true)); a failed check or install is
|
|
1042
|
+
// swallowed — getUpdateStatus()/getUpdateSettings() are the only surfaces,
|
|
1043
|
+
// there is no push notice channel from runtime -> TUI today.
|
|
1044
|
+
const updateBootTimer = setTimeout(() => {
|
|
1045
|
+
void (async () => {
|
|
1046
|
+
await checkForUpdateInternal({ force: false });
|
|
1047
|
+
if (autoUpdateEnabled() && updateCheckState.updateAvailable) {
|
|
1048
|
+
await runUpdateNowInternal();
|
|
1049
|
+
}
|
|
1050
|
+
})().catch(() => {});
|
|
1051
|
+
}, 0);
|
|
1052
|
+
updateBootTimer.unref?.();
|
|
1053
|
+
|
|
2687
1054
|
function mcpTransportLabel(cfg = {}) {
|
|
2688
1055
|
if (cfg.autoDetect) return `autoDetect:${cfg.autoDetect}`;
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
1056
|
+
try {
|
|
1057
|
+
return mcpClient.resolveMcpTransportKind(cfg);
|
|
1058
|
+
} catch {
|
|
1059
|
+
return 'unknown';
|
|
1060
|
+
}
|
|
2692
1061
|
}
|
|
2693
1062
|
|
|
2694
1063
|
function emitRuntimeNotification(content, meta = {}) {
|
|
@@ -2733,9 +1102,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2733
1102
|
}
|
|
2734
1103
|
|
|
2735
1104
|
function mcpStatus() {
|
|
2736
|
-
const
|
|
2737
|
-
? config.mcpServers
|
|
2738
|
-
: {};
|
|
1105
|
+
const { servers: configured, sources } = resolveEffectiveMcpServers();
|
|
2739
1106
|
const connected = new Map((mcpClient.getMcpServerStatus?.() || []).map((row) => [row.name, row]));
|
|
2740
1107
|
const failures = new Map((mcpFailures || []).map((row) => [row.name, row]));
|
|
2741
1108
|
const servers = [];
|
|
@@ -2752,6 +1119,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2752
1119
|
toolCount: live?.toolCount || 0,
|
|
2753
1120
|
tools: live?.tools || [],
|
|
2754
1121
|
error: fail?.msg || null,
|
|
1122
|
+
source: sources[name] || 'config',
|
|
2755
1123
|
});
|
|
2756
1124
|
connected.delete(name);
|
|
2757
1125
|
}
|
|
@@ -2866,6 +1234,26 @@ export async function createMixdogSessionRuntime({
|
|
|
2866
1234
|
} else {
|
|
2867
1235
|
scheduleCodeGraphPrewarm(changed ? 0 : codeGraphPrewarmDelayMs, changed ? 'cwd-change' : 'cwd');
|
|
2868
1236
|
}
|
|
1237
|
+
// Project-local `.mcp.json` follows the cwd: when the effective project MCP
|
|
1238
|
+
// set changes, reconnect in the background (never await — this stays sync,
|
|
1239
|
+
// and the session is preserved). Guarded so a no-op cwd change does not
|
|
1240
|
+
// churn connections.
|
|
1241
|
+
if (changed) {
|
|
1242
|
+
try {
|
|
1243
|
+
const nextKey = resolved + '\u0000' + JSON.stringify(readProjectMcpServers(resolved));
|
|
1244
|
+
if (nextKey !== lastProjectMcpKey) {
|
|
1245
|
+
lastProjectMcpKey = nextKey;
|
|
1246
|
+
void connectConfiguredMcp({ reset: true })
|
|
1247
|
+
.then(() => invalidatePreSessionToolSurface())
|
|
1248
|
+
.catch(() => {});
|
|
1249
|
+
}
|
|
1250
|
+
} catch {}
|
|
1251
|
+
}
|
|
1252
|
+
// CwdChanged: bridge an effective cwd switch to the standard hook bus.
|
|
1253
|
+
// No matcher event — payload is minimal { cwd }. Fire-and-forget.
|
|
1254
|
+
if (changed) {
|
|
1255
|
+
try { void hooks.dispatch('CwdChanged', hookCommonPayload({ cwd: currentCwd })); } catch {}
|
|
1256
|
+
}
|
|
2869
1257
|
return currentCwd;
|
|
2870
1258
|
}
|
|
2871
1259
|
|
|
@@ -2879,17 +1267,23 @@ export async function createMixdogSessionRuntime({
|
|
|
2879
1267
|
}
|
|
2880
1268
|
|
|
2881
1269
|
function skillContent(name) {
|
|
2882
|
-
const
|
|
2883
|
-
? contextMod.
|
|
1270
|
+
const res = typeof contextMod.loadSkillResource === 'function'
|
|
1271
|
+
? contextMod.loadSkillResource(name, currentCwd)
|
|
2884
1272
|
: null;
|
|
2885
|
-
if (!
|
|
2886
|
-
return { name, content };
|
|
1273
|
+
if (!res) throw new Error(`skill not found: ${name}`);
|
|
1274
|
+
return { name, content: res.content, dir: res.dir };
|
|
2887
1275
|
}
|
|
2888
1276
|
|
|
2889
1277
|
function skillToolContent(name) {
|
|
2890
1278
|
const skill = skillContent(name);
|
|
2891
|
-
|
|
2892
|
-
|
|
1279
|
+
// Return the general tool envelope so the main/Lead session behaves the
|
|
1280
|
+
// same as agent-loop sessions: the model-visible tool_result is the short
|
|
1281
|
+
// stub (`Loaded skill: <name>`) and the full SKILL.md body is delivered
|
|
1282
|
+
// ONCE as a separate injected role:'user' message (newMessages). The
|
|
1283
|
+
// envelope passes through internal-tools._normalize untouched (it preserves
|
|
1284
|
+
// __toolEnvelope objects), and the agent loop's central normalizeToolEnvelope
|
|
1285
|
+
// splits it into stub + injected user body.
|
|
1286
|
+
return contextMod.buildSkillToolEnvelope(skill.name, skill.content, skill.dir);
|
|
2893
1287
|
}
|
|
2894
1288
|
|
|
2895
1289
|
function addProjectSkill(input = {}) {
|
|
@@ -2943,7 +1337,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2943
1337
|
mcpScript: mcpScriptForPlugin(root),
|
|
2944
1338
|
};
|
|
2945
1339
|
plugin.mcpServerName = pluginMcpServerName(plugin);
|
|
2946
|
-
plugin.mcpEnabled = Object.prototype.hasOwnProperty.call(configuredMcp, plugin.mcpServerName)
|
|
1340
|
+
plugin.mcpEnabled = Object.prototype.hasOwnProperty.call(configuredMcp, plugin.mcpServerName)
|
|
1341
|
+
|| Object.keys(configuredMcp).some((k) => k.startsWith(`${plugin.mcpServerName}--`));
|
|
2947
1342
|
plugins.push(plugin);
|
|
2948
1343
|
};
|
|
2949
1344
|
|
|
@@ -2964,19 +1359,53 @@ export async function createMixdogSessionRuntime({
|
|
|
2964
1359
|
};
|
|
2965
1360
|
}
|
|
2966
1361
|
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
1362
|
+
// Merge mixdog-config `agent.mcpServers` with project-local `.mcp.json`.
|
|
1363
|
+
// On name collision the project-local `.mcp.json` entry WINS (Claude Code
|
|
1364
|
+
// precedence: project > user config). `sources[name]` records each server's
|
|
1365
|
+
// origin ('config' | 'project') for status reporting.
|
|
1366
|
+
function resolveEffectiveMcpServers() {
|
|
1367
|
+
const configured = config?.mcpServers && typeof config.mcpServers === 'object'
|
|
2971
1368
|
? config.mcpServers
|
|
2972
1369
|
: {};
|
|
2973
|
-
|
|
1370
|
+
const project = readProjectMcpServers(currentCwd);
|
|
1371
|
+
const servers = { ...configured, ...project };
|
|
1372
|
+
const sources = {};
|
|
1373
|
+
for (const name of Object.keys(configured)) sources[name] = 'config';
|
|
1374
|
+
for (const name of Object.keys(project)) sources[name] = 'project';
|
|
1375
|
+
return { servers, sources };
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
async function connectConfiguredMcp({ reset = false } = {}) {
|
|
1379
|
+
// Serialize reconnects: boot connect, cwd-change reset, and rapid cwd
|
|
1380
|
+
// switches must never interleave their disconnect/connect phases, or an
|
|
1381
|
+
// older run finishing after a newer reset could re-add stale servers into
|
|
1382
|
+
// the shared client registry. Approach: a generation token + a single
|
|
1383
|
+
// in-flight promise. Each call bumps the generation, waits for any prior
|
|
1384
|
+
// run to finish, then bails if a newer call has superseded it — leaving the
|
|
1385
|
+
// latest requested effective-server-set in the registry.
|
|
1386
|
+
const gen = ++mcpConnectGeneration;
|
|
1387
|
+
if (mcpConnectInFlight) {
|
|
1388
|
+
try { await mcpConnectInFlight; } catch { /* prior run's failures already captured */ }
|
|
1389
|
+
}
|
|
1390
|
+
if (gen !== mcpConnectGeneration) return mcpStatus();
|
|
1391
|
+
const run = (async () => {
|
|
1392
|
+
if (reset) await mcpClient.disconnectAll?.();
|
|
1393
|
+
mcpFailures = [];
|
|
1394
|
+
const { servers } = resolveEffectiveMcpServers();
|
|
1395
|
+
if (Object.keys(servers).length === 0) return;
|
|
1396
|
+
try {
|
|
1397
|
+
await mcpClient.connectMcpServers(servers);
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
mcpFailures = Array.isArray(error?.failures)
|
|
1400
|
+
? error.failures
|
|
1401
|
+
: [{ name: 'mcp', msg: error?.message || String(error) }];
|
|
1402
|
+
}
|
|
1403
|
+
})();
|
|
1404
|
+
mcpConnectInFlight = run;
|
|
2974
1405
|
try {
|
|
2975
|
-
await
|
|
2976
|
-
}
|
|
2977
|
-
|
|
2978
|
-
? error.failures
|
|
2979
|
-
: [{ name: 'mcp', msg: error?.message || String(error) }];
|
|
1406
|
+
await run;
|
|
1407
|
+
} finally {
|
|
1408
|
+
if (mcpConnectInFlight === run) mcpConnectInFlight = null;
|
|
2980
1409
|
}
|
|
2981
1410
|
return mcpStatus();
|
|
2982
1411
|
}
|
|
@@ -2984,10 +1413,42 @@ export async function createMixdogSessionRuntime({
|
|
|
2984
1413
|
function normalizeMcpServerInput(input = {}) {
|
|
2985
1414
|
const name = clean(input.name).toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
2986
1415
|
if (!name) throw new Error('MCP server name is required');
|
|
1416
|
+
const coerceStringRecord = (value) => {
|
|
1417
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
1418
|
+
const out = {};
|
|
1419
|
+
for (const [key, val] of Object.entries(value)) {
|
|
1420
|
+
if (val === undefined || val === null) continue;
|
|
1421
|
+
out[String(key)] = String(val);
|
|
1422
|
+
}
|
|
1423
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
1424
|
+
};
|
|
1425
|
+
const withOptionalHeaders = (config) => {
|
|
1426
|
+
const headers = coerceStringRecord(input.headers);
|
|
1427
|
+
if (headers) config.headers = headers;
|
|
1428
|
+
return config;
|
|
1429
|
+
};
|
|
2987
1430
|
const url = clean(input.url);
|
|
1431
|
+
const type = clean(input.type).toLowerCase();
|
|
2988
1432
|
if (url) {
|
|
1433
|
+
if (type === 'sse') {
|
|
1434
|
+
if (!/^https?:\/\//i.test(url)) throw new Error('MCP URL must start with http:// or https://');
|
|
1435
|
+
return { name, config: withOptionalHeaders({ type: 'sse', url }) };
|
|
1436
|
+
}
|
|
1437
|
+
if (type === 'ws') {
|
|
1438
|
+
if (!/^(?:wss?|https?):\/\//i.test(url)) {
|
|
1439
|
+
throw new Error('MCP WebSocket URL must start with ws://, wss://, http://, or https://');
|
|
1440
|
+
}
|
|
1441
|
+
return { name, config: withOptionalHeaders({ type: 'ws', url }) };
|
|
1442
|
+
}
|
|
1443
|
+
if (type === 'http' || type === 'streamable-http') {
|
|
1444
|
+
if (!/^https?:\/\//i.test(url)) throw new Error('MCP URL must start with http:// or https://');
|
|
1445
|
+
return { name, config: withOptionalHeaders({ type: 'http', url }) };
|
|
1446
|
+
}
|
|
1447
|
+
if (/^wss?:\/\//i.test(url)) {
|
|
1448
|
+
return { name, config: withOptionalHeaders({ type: 'ws', url }) };
|
|
1449
|
+
}
|
|
2989
1450
|
if (!/^https?:\/\//i.test(url)) throw new Error('MCP URL must start with http:// or https://');
|
|
2990
|
-
return { name, config: {
|
|
1451
|
+
return { name, config: withOptionalHeaders({ type: 'http', url }) };
|
|
2991
1452
|
}
|
|
2992
1453
|
const command = clean(input.command);
|
|
2993
1454
|
if (!command) throw new Error('MCP server command or URL is required');
|
|
@@ -3001,7 +1462,10 @@ export async function createMixdogSessionRuntime({
|
|
|
3001
1462
|
if (resolvedCwd !== root && !resolvedCwd.startsWith(`${root}\\`) && !resolvedCwd.startsWith(`${root}/`)) {
|
|
3002
1463
|
throw new Error('MCP server cwd must stay under the current project');
|
|
3003
1464
|
}
|
|
3004
|
-
|
|
1465
|
+
const config = { type: 'stdio', command, args, cwd: resolvedCwd };
|
|
1466
|
+
const env = coerceStringRecord(input.env);
|
|
1467
|
+
if (env) config.env = env;
|
|
1468
|
+
return { name, config };
|
|
3005
1469
|
}
|
|
3006
1470
|
|
|
3007
1471
|
const agentToolStartedAt = performance.now();
|
|
@@ -3011,6 +1475,18 @@ export async function createMixdogSessionRuntime({
|
|
|
3011
1475
|
mgr,
|
|
3012
1476
|
dataDir: cfgMod.getPluginData(),
|
|
3013
1477
|
cwd,
|
|
1478
|
+
// SubagentStart/SubagentStop: bridge internal worker spawn/finish to the
|
|
1479
|
+
// standard hook bus. agent_type is passed top-level via hookCommonPayload
|
|
1480
|
+
// (added to hook-bus buildEventPayload passthrough). Best-effort.
|
|
1481
|
+
onSubagentEvent: (phase, info = {}) => {
|
|
1482
|
+
try {
|
|
1483
|
+
const event = phase === 'stop' ? 'SubagentStop' : 'SubagentStart';
|
|
1484
|
+
void hooks.dispatch(event, hookCommonPayload({
|
|
1485
|
+
session_id: info?.session_id || null,
|
|
1486
|
+
agent_type: info?.agent_type || null,
|
|
1487
|
+
}));
|
|
1488
|
+
} catch { /* best-effort: subagent hook must never affect worker lifecycle */ }
|
|
1489
|
+
},
|
|
3014
1490
|
});
|
|
3015
1491
|
bootProfile('agent:ready', { ms: (performance.now() - agentToolStartedAt).toFixed(1) });
|
|
3016
1492
|
const agentStatusState = () => {
|
|
@@ -3035,10 +1511,12 @@ export async function createMixdogSessionRuntime({
|
|
|
3035
1511
|
if (msg?.method !== 'notifications/claude/channel') return;
|
|
3036
1512
|
const params = msg?.params && typeof msg.params === 'object' ? msg.params : {};
|
|
3037
1513
|
const meta = params.meta && typeof params.meta === 'object' ? params.meta : {};
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
const
|
|
3041
|
-
|
|
1514
|
+
const content = channelNotificationModelContent(params);
|
|
1515
|
+
if (!content) return;
|
|
1516
|
+
const handled = emitRuntimeNotification(content, meta);
|
|
1517
|
+
if (!handled && session?.id && shouldMirrorChannelNotificationToPending(meta)) {
|
|
1518
|
+
try { mgr.enqueuePendingMessage(session.id, content); } catch {}
|
|
1519
|
+
}
|
|
3042
1520
|
},
|
|
3043
1521
|
});
|
|
3044
1522
|
bootProfile('channels:worker-ready', { ms: (performance.now() - channelsStartedAt).toFixed(1) });
|
|
@@ -3169,6 +1647,7 @@ export async function createMixdogSessionRuntime({
|
|
|
3169
1647
|
},
|
|
3170
1648
|
});
|
|
3171
1649
|
internalTools.markBootReady?.();
|
|
1650
|
+
try { lastProjectMcpKey = resolve(currentCwd) + '\u0000' + JSON.stringify(readProjectMcpServers(currentCwd)); } catch { lastProjectMcpKey = null; }
|
|
3172
1651
|
void connectConfiguredMcp()
|
|
3173
1652
|
.then((status) => bootProfile('mcp:ready', {
|
|
3174
1653
|
connected: Number(status?.connectedCount || 0),
|
|
@@ -3183,8 +1662,8 @@ export async function createMixdogSessionRuntime({
|
|
|
3183
1662
|
function invalidateProviderCaches() {
|
|
3184
1663
|
providerModelsCache = { models: null, at: 0 };
|
|
3185
1664
|
providerModelsPromise = null;
|
|
1665
|
+
providerModelsLoadSeq += 1;
|
|
3186
1666
|
searchProviderModelsCache = { models: null, at: 0 };
|
|
3187
|
-
searchProviderModelsPromise = null;
|
|
3188
1667
|
usageDashboardCache = { dashboard: null, at: 0 };
|
|
3189
1668
|
usageDashboardPromise = null;
|
|
3190
1669
|
providerSetupCache = { setup: null, at: 0 };
|
|
@@ -3216,6 +1695,58 @@ export async function createMixdogSessionRuntime({
|
|
|
3216
1695
|
let pendingConfigToSave = null;
|
|
3217
1696
|
let configSaveTimer = null;
|
|
3218
1697
|
const CONFIG_SAVE_DEBOUNCE_MS = 150;
|
|
1698
|
+
// Same debounce pattern as saveConfigAndAdopt below, but for the channels-
|
|
1699
|
+
// section backend switch: channel-admin's setBackend() does a synchronous
|
|
1700
|
+
// file-locked read-modify-write (updateChannelsSection), which is a
|
|
1701
|
+
// secondary hitch source on the settings-toggle key handler. Coalesce
|
|
1702
|
+
// rapid toggles and move the write off the key-handler tick.
|
|
1703
|
+
let pendingBackendName = null;
|
|
1704
|
+
let backendSaveTimer = null;
|
|
1705
|
+
function flushBackendSave() {
|
|
1706
|
+
if (backendSaveTimer) {
|
|
1707
|
+
clearTimeout(backendSaveTimer);
|
|
1708
|
+
backendSaveTimer = null;
|
|
1709
|
+
}
|
|
1710
|
+
if (pendingBackendName === null) return;
|
|
1711
|
+
const name = pendingBackendName;
|
|
1712
|
+
pendingBackendName = null;
|
|
1713
|
+
try {
|
|
1714
|
+
setBackend(name);
|
|
1715
|
+
} catch (err) {
|
|
1716
|
+
process.stderr.write(`[channels] debounced setBackend failed: ${err?.message || err}\n`);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
// Debounced persist for the top-level `outputStyle` config key. This CANNOT
|
|
1720
|
+
// ride saveConfigAndAdopt/flushConfigSave: cfgMod.saveConfig() serializes
|
|
1721
|
+
// ONLY the agent-section fields (see orchestrator config.mjs saveConfig),
|
|
1722
|
+
// so a top-level outputStyle adopted in memory would never reach disk and
|
|
1723
|
+
// would silently revert on the next fresh catalog read/restart. Persist it
|
|
1724
|
+
// through sharedCfgMod.updateConfig (whole-root RMW under the same file
|
|
1725
|
+
// lock), debounced off the key-handler tick like the backend switch above.
|
|
1726
|
+
let pendingOutputStyleId = null;
|
|
1727
|
+
let outputStyleSaveTimer = null;
|
|
1728
|
+
function flushOutputStyleSave() {
|
|
1729
|
+
if (outputStyleSaveTimer) {
|
|
1730
|
+
clearTimeout(outputStyleSaveTimer);
|
|
1731
|
+
outputStyleSaveTimer = null;
|
|
1732
|
+
}
|
|
1733
|
+
if (pendingOutputStyleId === null) return;
|
|
1734
|
+
const styleId = pendingOutputStyleId;
|
|
1735
|
+
pendingOutputStyleId = null;
|
|
1736
|
+
try {
|
|
1737
|
+
sharedCfgMod.updateConfig((root) => {
|
|
1738
|
+
const next = { ...(root || {}), outputStyle: styleId };
|
|
1739
|
+
if (next.agent && typeof next.agent === 'object' && !Array.isArray(next.agent)) {
|
|
1740
|
+
const agent = { ...next.agent };
|
|
1741
|
+
delete agent.outputStyle;
|
|
1742
|
+
next.agent = agent;
|
|
1743
|
+
}
|
|
1744
|
+
return next;
|
|
1745
|
+
});
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
process.stderr.write(`[config] debounced outputStyle save failed: ${err?.message || err}\n`);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
3219
1750
|
function flushConfigSave() {
|
|
3220
1751
|
if (configSaveTimer) {
|
|
3221
1752
|
clearTimeout(configSaveTimer);
|
|
@@ -3631,17 +2162,11 @@ function parsedProviderModelVersion(id) {
|
|
|
3631
2162
|
searchProviderModelsCache = { models: rows, at: Date.now() };
|
|
3632
2163
|
return providerModelsFromCacheRows(rows);
|
|
3633
2164
|
}
|
|
3634
|
-
if (
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
return models;
|
|
3639
|
-
})
|
|
3640
|
-
.finally(() => {
|
|
3641
|
-
searchProviderModelsPromise = null;
|
|
3642
|
-
});
|
|
2165
|
+
if (force) {
|
|
2166
|
+
const models = await loadSearchProviderModelsFresh({ forceRefresh: true });
|
|
2167
|
+
searchProviderModelsCache = { models, at: Date.now() };
|
|
2168
|
+
return providerModelsFromCacheRows(models);
|
|
3643
2169
|
}
|
|
3644
|
-
return providerModelsFromCacheRows(await searchProviderModelsPromise);
|
|
3645
2170
|
}
|
|
3646
2171
|
|
|
3647
2172
|
function enabledSearchProviderConfig() {
|
|
@@ -3655,7 +2180,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3655
2180
|
return out;
|
|
3656
2181
|
}
|
|
3657
2182
|
|
|
3658
|
-
async function loadSearchProviderModelsFresh() {
|
|
2183
|
+
async function loadSearchProviderModelsFresh({ forceRefresh = false } = {}) {
|
|
3659
2184
|
const searchProviders = enabledSearchProviderConfig();
|
|
3660
2185
|
const providerNames = Object.keys(searchProviders);
|
|
3661
2186
|
if (!providerNames.length) return [];
|
|
@@ -3664,7 +2189,13 @@ function parsedProviderModelVersion(id) {
|
|
|
3664
2189
|
const provider = reg.getProvider(name);
|
|
3665
2190
|
if (typeof provider?.listModels !== 'function') return [];
|
|
3666
2191
|
try {
|
|
3667
|
-
|
|
2192
|
+
let models = null;
|
|
2193
|
+
if (forceRefresh && typeof provider._refreshModelCache === 'function') {
|
|
2194
|
+
models = await provider._refreshModelCache();
|
|
2195
|
+
}
|
|
2196
|
+
if (!Array.isArray(models)) {
|
|
2197
|
+
models = await provider.listModels();
|
|
2198
|
+
}
|
|
3668
2199
|
if (!Array.isArray(models)) return [];
|
|
3669
2200
|
const rows = [];
|
|
3670
2201
|
for (const m of models) {
|
|
@@ -3698,14 +2229,20 @@ function parsedProviderModelVersion(id) {
|
|
|
3698
2229
|
return results;
|
|
3699
2230
|
}
|
|
3700
2231
|
|
|
3701
|
-
async function loadProviderModelsFresh() {
|
|
2232
|
+
async function loadProviderModelsFresh({ forceRefresh = false } = {}) {
|
|
3702
2233
|
ensureFullConfig();
|
|
3703
2234
|
await ensureProvidersReady(config.providers || {});
|
|
3704
2235
|
const allProviders = [...reg.getAllProviders()];
|
|
3705
2236
|
const providerResults = await Promise.all(allProviders.map(async ([name, provider]) => {
|
|
3706
2237
|
if (typeof provider?.listModels !== 'function') return [];
|
|
3707
2238
|
try {
|
|
3708
|
-
|
|
2239
|
+
let models = null;
|
|
2240
|
+
if (forceRefresh && typeof provider._refreshModelCache === 'function') {
|
|
2241
|
+
models = await provider._refreshModelCache();
|
|
2242
|
+
}
|
|
2243
|
+
if (!Array.isArray(models)) {
|
|
2244
|
+
models = await provider.listModels();
|
|
2245
|
+
}
|
|
3709
2246
|
if (!Array.isArray(models)) return [];
|
|
3710
2247
|
const rows = [];
|
|
3711
2248
|
for (const m of models) {
|
|
@@ -3740,10 +2277,17 @@ function parsedProviderModelVersion(id) {
|
|
|
3740
2277
|
warmProviderModelCache();
|
|
3741
2278
|
return quickProviderModelRows();
|
|
3742
2279
|
}
|
|
2280
|
+
if (force) {
|
|
2281
|
+
const seq = ++providerModelsLoadSeq;
|
|
2282
|
+
const models = await loadProviderModelsFresh({ forceRefresh: true });
|
|
2283
|
+
if (seq === providerModelsLoadSeq) providerModelsCache = { models, at: Date.now() };
|
|
2284
|
+
return providerModelsFromCacheRows(models);
|
|
2285
|
+
}
|
|
3743
2286
|
if (!providerModelsPromise) {
|
|
2287
|
+
const seq = ++providerModelsLoadSeq;
|
|
3744
2288
|
providerModelsPromise = loadProviderModelsFresh()
|
|
3745
2289
|
.then((models) => {
|
|
3746
|
-
providerModelsCache = { models, at: Date.now() };
|
|
2290
|
+
if (seq === providerModelsLoadSeq) providerModelsCache = { models, at: Date.now() };
|
|
3747
2291
|
return models;
|
|
3748
2292
|
})
|
|
3749
2293
|
.finally(() => {
|
|
@@ -3755,9 +2299,10 @@ function parsedProviderModelVersion(id) {
|
|
|
3755
2299
|
|
|
3756
2300
|
function warmProviderModelCache() {
|
|
3757
2301
|
if (Array.isArray(providerModelsCache.models) || providerModelsPromise) return providerModelsPromise;
|
|
2302
|
+
const seq = ++providerModelsLoadSeq;
|
|
3758
2303
|
providerModelsPromise = loadProviderModelsFresh()
|
|
3759
2304
|
.then((models) => {
|
|
3760
|
-
providerModelsCache = { models, at: Date.now() };
|
|
2305
|
+
if (seq === providerModelsLoadSeq) providerModelsCache = { models, at: Date.now() };
|
|
3761
2306
|
bootProfile('provider-models:warm-ready', { count: models.length });
|
|
3762
2307
|
return models;
|
|
3763
2308
|
})
|
|
@@ -3794,12 +2339,22 @@ function parsedProviderModelVersion(id) {
|
|
|
3794
2339
|
const requested = hasOwn(route, 'effort') ? route.effort : (route.preset?.effort || null);
|
|
3795
2340
|
const effectiveEffort = coerceEffortFor(route.provider, modelMeta, requested);
|
|
3796
2341
|
const fastCapable = fastCapableFor(route.provider, modelMeta);
|
|
2342
|
+
// Carry the catalog display name onto the route so the statusline shows a
|
|
2343
|
+
// human label (e.g. "Claude Fable 5") for preset-less direct models instead
|
|
2344
|
+
// of the raw id. `name` is only trusted when it differs from the raw model
|
|
2345
|
+
// id (some providers echo the id as `name`), so it can't clobber a better
|
|
2346
|
+
// already-resolved label. Falls back to existing route.modelDisplay, then unset.
|
|
2347
|
+
const metaName = clean(modelMeta?.name);
|
|
2348
|
+
const modelDisplay = clean(modelMeta?.display) || clean(modelMeta?.displayName)
|
|
2349
|
+
|| (metaName && metaName !== clean(route.model) ? metaName : '')
|
|
2350
|
+
|| clean(route.modelDisplay);
|
|
3797
2351
|
route = {
|
|
3798
2352
|
...route,
|
|
3799
2353
|
fast: fastCapable ? route.fast === true : false,
|
|
3800
2354
|
fastCapable,
|
|
3801
2355
|
effectiveEffort,
|
|
3802
2356
|
effortOptions: effortItemsFor(route.provider, modelMeta, effectiveEffort),
|
|
2357
|
+
...(modelDisplay ? { modelDisplay } : {}),
|
|
3803
2358
|
};
|
|
3804
2359
|
return route;
|
|
3805
2360
|
}
|
|
@@ -3857,7 +2412,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3857
2412
|
preset: route.preset || undefined,
|
|
3858
2413
|
tools: toolSpecForMode(mode),
|
|
3859
2414
|
owner: 'cli',
|
|
3860
|
-
|
|
2415
|
+
agent: 'lead',
|
|
3861
2416
|
lane: 'cli',
|
|
3862
2417
|
sourceType: 'lead',
|
|
3863
2418
|
sourceName: 'main',
|
|
@@ -3906,11 +2461,61 @@ function parsedProviderModelVersion(id) {
|
|
|
3906
2461
|
configurable: true,
|
|
3907
2462
|
writable: true,
|
|
3908
2463
|
});
|
|
2464
|
+
// PostToolUseFailure: dispatched by loop.mjs only when a tool execution
|
|
2465
|
+
// resolved to a failure (thrown-error path or an is_error result). Same
|
|
2466
|
+
// shape as afterToolHook; `result` carries the error text. Best-effort.
|
|
2467
|
+
Object.defineProperty(session, 'afterToolFailureHook', {
|
|
2468
|
+
value: (input) => hooks.dispatch('PostToolUseFailure', hookCommonPayload({
|
|
2469
|
+
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2470
|
+
cwd: input?.cwd || currentCwd,
|
|
2471
|
+
tool_name: input?.name,
|
|
2472
|
+
tool_input: input?.args,
|
|
2473
|
+
tool_use_id: input?.toolCallId || input?.tool_use_id,
|
|
2474
|
+
tool_response: input?.result,
|
|
2475
|
+
})),
|
|
2476
|
+
enumerable: false,
|
|
2477
|
+
configurable: true,
|
|
2478
|
+
writable: true,
|
|
2479
|
+
});
|
|
2480
|
+
// PostToolBatch: dispatched by loop.mjs after a full parallel batch of
|
|
2481
|
+
// tool calls resolves and before the next model call. No matcher event.
|
|
2482
|
+
Object.defineProperty(session, 'afterToolBatchHook', {
|
|
2483
|
+
value: (input) => hooks.dispatch('PostToolBatch', hookCommonPayload({
|
|
2484
|
+
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2485
|
+
cwd: input?.cwd || currentCwd,
|
|
2486
|
+
})),
|
|
2487
|
+
enumerable: false,
|
|
2488
|
+
configurable: true,
|
|
2489
|
+
writable: true,
|
|
2490
|
+
});
|
|
2491
|
+
// PreCompact / PostCompact: dispatched by manager.mjs/loop.mjs compaction
|
|
2492
|
+
// flow via these session-property hooks (manager has no hooks bus access).
|
|
2493
|
+
// payload { trigger: 'auto' | 'manual' }. Best-effort.
|
|
2494
|
+
Object.defineProperty(session, 'preCompactHook', {
|
|
2495
|
+
value: (input) => hooks.dispatch('PreCompact', hookCommonPayload({
|
|
2496
|
+
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2497
|
+
cwd: input?.cwd || currentCwd,
|
|
2498
|
+
trigger: input?.trigger || 'auto',
|
|
2499
|
+
})),
|
|
2500
|
+
enumerable: false,
|
|
2501
|
+
configurable: true,
|
|
2502
|
+
writable: true,
|
|
2503
|
+
});
|
|
2504
|
+
Object.defineProperty(session, 'postCompactHook', {
|
|
2505
|
+
value: (input) => hooks.dispatch('PostCompact', hookCommonPayload({
|
|
2506
|
+
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2507
|
+
cwd: input?.cwd || currentCwd,
|
|
2508
|
+
trigger: input?.trigger || 'auto',
|
|
2509
|
+
})),
|
|
2510
|
+
enumerable: false,
|
|
2511
|
+
configurable: true,
|
|
2512
|
+
writable: true,
|
|
2513
|
+
});
|
|
3909
2514
|
applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
3910
2515
|
applyPreSessionToolSelection();
|
|
3911
2516
|
writeStatuslineRoute(statusRoutes, session, route);
|
|
3912
2517
|
hooks.emit('session:create', { sessionId: session.id, provider: route.provider, model: route.model, toolMode: mode, cwd: currentCwd });
|
|
3913
|
-
// SessionStart: bridge to the standard
|
|
2518
|
+
// SessionStart: bridge to the standard project hook bus. Best-effort;
|
|
3914
2519
|
// a hook error must never break session creation. additionalContext is
|
|
3915
2520
|
// injected before the first user turn as a system-reminder context pair.
|
|
3916
2521
|
try {
|
|
@@ -4021,6 +2626,27 @@ function parsedProviderModelVersion(id) {
|
|
|
4021
2626
|
providerModelWarmupTimer.unref?.();
|
|
4022
2627
|
}
|
|
4023
2628
|
|
|
2629
|
+
function scheduleModelCatalogWarmup(delayMs = modelCatalogWarmupDelayMs) {
|
|
2630
|
+
if (!modelCatalogWarmupEnabled) {
|
|
2631
|
+
bootProfile('model-catalog:warm-skipped', { reason: 'disabled' });
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
if (modelCatalogWarmupTimer || closeRequested) return;
|
|
2635
|
+
modelCatalogWarmupTimer = setTimeout(() => {
|
|
2636
|
+
modelCatalogWarmupTimer = null;
|
|
2637
|
+
if (closeRequested) return;
|
|
2638
|
+
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
2639
|
+
bootProfile('model-catalog:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
2640
|
+
scheduleModelCatalogWarmup(backgroundBusyRetryMs);
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
void warmCatalogsInBackground()
|
|
2644
|
+
.then(() => bootProfile('model-catalog:warm-ready'))
|
|
2645
|
+
.catch((error) => bootProfile('model-catalog:warm-failed', { error: error?.message || String(error) }));
|
|
2646
|
+
}, delayMs);
|
|
2647
|
+
modelCatalogWarmupTimer.unref?.();
|
|
2648
|
+
}
|
|
2649
|
+
|
|
4024
2650
|
function scheduleStatuslineUsageWarmup(delayMs = statuslineUsageWarmupDelayMs) {
|
|
4025
2651
|
const providerId = clean(route?.provider);
|
|
4026
2652
|
if (!providerId || !providerId.includes('oauth')) {
|
|
@@ -4081,6 +2707,22 @@ function parsedProviderModelVersion(id) {
|
|
|
4081
2707
|
statuslineUsageRefreshTimer.unref?.();
|
|
4082
2708
|
}
|
|
4083
2709
|
|
|
2710
|
+
function invokeChannelStart() {
|
|
2711
|
+
if (channelStartPromise) return channelStartPromise;
|
|
2712
|
+
const startedAt = performance.now();
|
|
2713
|
+
bootProfile('channels:start:begin');
|
|
2714
|
+
channelStartPromise = channels.start()
|
|
2715
|
+
.then(() => bootProfile('channels:start:ready', { ms: (performance.now() - startedAt).toFixed(1) }))
|
|
2716
|
+
.catch((error) => bootProfile('channels:start:failed', {
|
|
2717
|
+
ms: (performance.now() - startedAt).toFixed(1),
|
|
2718
|
+
error: error?.message || String(error),
|
|
2719
|
+
}))
|
|
2720
|
+
.finally(() => {
|
|
2721
|
+
channelStartPromise = null;
|
|
2722
|
+
});
|
|
2723
|
+
return channelStartPromise;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
4084
2726
|
function scheduleChannelStart(delayMs = channelStartDelayMs) {
|
|
4085
2727
|
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
4086
2728
|
bootProfile('channels:start-skipped');
|
|
@@ -4090,28 +2732,63 @@ function parsedProviderModelVersion(id) {
|
|
|
4090
2732
|
bootProfile('channels:start-disabled');
|
|
4091
2733
|
return;
|
|
4092
2734
|
}
|
|
4093
|
-
if (channelStartTimer || closeRequested) return;
|
|
2735
|
+
if (channelStartTimer || channelStartPromise || closeRequested) return;
|
|
4094
2736
|
bootProfile('channels:start-scheduled', { delayMs });
|
|
4095
2737
|
channelStartTimer = setTimeout(() => {
|
|
4096
2738
|
channelStartTimer = null;
|
|
4097
2739
|
if (closeRequested) return;
|
|
2740
|
+
// A deferred start may straddle a stopRemote(); re-check before booting so
|
|
2741
|
+
// a turned-off session neither starts channels nor keeps rescheduling.
|
|
2742
|
+
if (!remoteEnabled) return;
|
|
4098
2743
|
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
4099
2744
|
bootProfile('channels:start-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
4100
2745
|
scheduleChannelStart(backgroundBusyRetryMs);
|
|
4101
2746
|
return;
|
|
4102
2747
|
}
|
|
4103
|
-
|
|
4104
|
-
bootProfile('channels:start:begin');
|
|
4105
|
-
channels.start()
|
|
4106
|
-
.then(() => bootProfile('channels:start:ready', { ms: (performance.now() - startedAt).toFixed(1) }))
|
|
4107
|
-
.catch((error) => bootProfile('channels:start:failed', {
|
|
4108
|
-
ms: (performance.now() - startedAt).toFixed(1),
|
|
4109
|
-
error: error?.message || String(error),
|
|
4110
|
-
}));
|
|
2748
|
+
void invokeChannelStart();
|
|
4111
2749
|
}, delayMs);
|
|
4112
2750
|
channelStartTimer.unref?.();
|
|
4113
2751
|
}
|
|
4114
2752
|
|
|
2753
|
+
// Remote (Discord channel) mode is opt-in per session. Only a session that
|
|
2754
|
+
// explicitly enables remote — via `mixdog --remote` or the runtime toggle —
|
|
2755
|
+
// boots the channel worker and contends for channel ownership.
|
|
2756
|
+
function startRemote() {
|
|
2757
|
+
remoteEnabled = true;
|
|
2758
|
+
// A backend switch may still be sitting in its debounce window; flush it
|
|
2759
|
+
// so the channel worker boots against the backend the user just chose,
|
|
2760
|
+
// not the previous on-disk value.
|
|
2761
|
+
try { flushBackendSave(); } catch {}
|
|
2762
|
+
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
2763
|
+
bootProfile('channels:start-skipped');
|
|
2764
|
+
return true;
|
|
2765
|
+
}
|
|
2766
|
+
if (!channelsEnabled()) {
|
|
2767
|
+
bootProfile('channels:start-disabled');
|
|
2768
|
+
return true;
|
|
2769
|
+
}
|
|
2770
|
+
if (closeRequested) return true;
|
|
2771
|
+
if (channelStartTimer) {
|
|
2772
|
+
clearTimeout(channelStartTimer);
|
|
2773
|
+
channelStartTimer = null;
|
|
2774
|
+
}
|
|
2775
|
+
bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
|
|
2776
|
+
void invokeChannelStart();
|
|
2777
|
+
return true;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
function stopRemote(reason) {
|
|
2781
|
+
remoteEnabled = false;
|
|
2782
|
+
// Cancel any pending deferred start so it can't fire after remote is off.
|
|
2783
|
+
if (channelStartTimer) { clearTimeout(channelStartTimer); channelStartTimer = null; }
|
|
2784
|
+
channels.stop(reason || 'remote-disabled', { waitForExit: false }).catch(() => {});
|
|
2785
|
+
return true;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
function isRemoteEnabled() {
|
|
2789
|
+
return remoteEnabled;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
4115
2792
|
function withTeardownDeadline(promise, ms, fallback = false) {
|
|
4116
2793
|
let timer = null;
|
|
4117
2794
|
return Promise.race([
|
|
@@ -4139,6 +2816,7 @@ function parsedProviderModelVersion(id) {
|
|
|
4139
2816
|
bootProfile('code-graph:prewarm-lazy', { reason: 'startup-deferred-to-first-turn' });
|
|
4140
2817
|
}
|
|
4141
2818
|
scheduleProviderSetupWarmup();
|
|
2819
|
+
scheduleModelCatalogWarmup();
|
|
4142
2820
|
scheduleProviderWarmup();
|
|
4143
2821
|
// Warm the provider model catalog in the background, but keep it on its own
|
|
4144
2822
|
// delay so short-lived detached runtimes can exit before /models I/O starts.
|
|
@@ -4146,7 +2824,10 @@ function parsedProviderModelVersion(id) {
|
|
|
4146
2824
|
// MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS explicitly.
|
|
4147
2825
|
scheduleProviderModelWarmup();
|
|
4148
2826
|
scheduleStatuslineUsageWarmup();
|
|
4149
|
-
|
|
2827
|
+
// Channels are opt-in: only boot the worker when this session started in (or
|
|
2828
|
+
// was toggled into) remote mode. Non-remote sessions never contend for the
|
|
2829
|
+
// channel; see startRemote()/stopRemote() and the `/remote` toggle.
|
|
2830
|
+
if (remoteEnabled) startRemote();
|
|
4150
2831
|
|
|
4151
2832
|
function contextStatusCacheKeyFor({ messages, tools }) {
|
|
4152
2833
|
const compaction = session?.compaction || {};
|
|
@@ -4173,10 +2854,12 @@ function parsedProviderModelVersion(id) {
|
|
|
4173
2854
|
lastContextTokensUpdatedAt: Number(session?.lastContextTokensUpdatedAt || 0),
|
|
4174
2855
|
lastContextTokensStaleAfterCompact: session?.lastContextTokensStaleAfterCompact === true,
|
|
4175
2856
|
lastInputTokens: Number(session?.lastInputTokens || 0),
|
|
2857
|
+
lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
|
|
4176
2858
|
lastOutputTokens: Number(session?.lastOutputTokens || 0),
|
|
4177
2859
|
lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
|
|
4178
2860
|
lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
|
|
4179
2861
|
totalInputTokens: Number(session?.totalInputTokens || 0),
|
|
2862
|
+
totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
|
|
4180
2863
|
totalOutputTokens: Number(session?.totalOutputTokens || 0),
|
|
4181
2864
|
totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
|
|
4182
2865
|
totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
|
|
@@ -4299,38 +2982,13 @@ function parsedProviderModelVersion(id) {
|
|
|
4299
2982
|
// the gauge numerator.
|
|
4300
2983
|
const usedTokens = estimatedContextTokens;
|
|
4301
2984
|
const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
|
|
4302
|
-
|
|
4303
|
-
//
|
|
4304
|
-
//
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
// An explicit autoCompactTokenLimit (provider/catalog/seed supplied)
|
|
4308
|
-
// still takes precedence and is honored verbatim when it sits at or below
|
|
4309
|
-
// the boundary.
|
|
4310
|
-
const defaultCompactBufferTokens = contextDefaultBufferTokens(compactBoundaryTokens, session?.compaction || {});
|
|
4311
|
-
const defaultCompactTriggerTokens = compactBoundaryTokens
|
|
4312
|
-
? Math.max(1, compactBoundaryTokens - defaultCompactBufferTokens)
|
|
4313
|
-
: 0;
|
|
4314
|
-
// Only an explicit auto-compact limit STRICTLY BELOW the boundary is a
|
|
4315
|
-
// real trigger; a persisted value == boundary is a legacy derived
|
|
4316
|
-
// full-window artifact and must fall through to the default trigger.
|
|
4317
|
-
// Likewise ignore persisted default-buffer telemetry so the displayed
|
|
4318
|
-
// trigger matches what the runtime now actually fires.
|
|
4319
|
-
const persistedTriggerTokens = Number(session?.compaction?.triggerTokens || 0);
|
|
4320
|
-
const legacyDefaultTriggerTelemetry = isLegacyDefaultContextBufferTelemetry(session?.compaction || {}, compactBoundaryTokens)
|
|
4321
|
-
&& persistedTriggerTokens > 0
|
|
4322
|
-
&& persistedTriggerTokens < compactBoundaryTokens;
|
|
4323
|
-
const usablePersistedTrigger = persistedTriggerTokens > 0
|
|
4324
|
-
&& !legacyDefaultTriggerTelemetry
|
|
4325
|
-
&& (!compactBoundaryTokens || persistedTriggerTokens < compactBoundaryTokens)
|
|
4326
|
-
? persistedTriggerTokens
|
|
4327
|
-
: 0;
|
|
4328
|
-
const compactTriggerTokens = autoCompactTokenLimit && compactBoundaryTokens && autoCompactTokenLimit < compactBoundaryTokens
|
|
4329
|
-
? autoCompactTokenLimit
|
|
4330
|
-
: (usablePersistedTrigger || defaultCompactTriggerTokens || 0);
|
|
4331
|
-
const compactBufferTokens = Number(compactBoundaryTokens && compactTriggerTokens
|
|
2985
|
+
// Use the same shared compact-policy math as manager/loop. Do not trust
|
|
2986
|
+
// persisted trigger telemetry as an independent policy input: it is an
|
|
2987
|
+
// output snapshot and was the source of repeated /context false positives.
|
|
2988
|
+
const compactTriggerTokens = resolveCompactTriggerTokens(session || {}, compactBoundaryTokens) || 0;
|
|
2989
|
+
const compactBufferTokens = compactBoundaryTokens
|
|
4332
2990
|
? Math.max(0, compactBoundaryTokens - compactTriggerTokens)
|
|
4333
|
-
:
|
|
2991
|
+
: resolveCompactBufferTokens(compactBoundaryTokens, session?.compaction || {});
|
|
4334
2992
|
const value = {
|
|
4335
2993
|
sessionId: session?.id || null,
|
|
4336
2994
|
provider: route.provider,
|
|
@@ -4365,11 +3023,13 @@ function parsedProviderModelVersion(id) {
|
|
|
4365
3023
|
},
|
|
4366
3024
|
usage: {
|
|
4367
3025
|
lastInputTokens: Number(session?.lastInputTokens || 0),
|
|
3026
|
+
lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
|
|
4368
3027
|
lastOutputTokens: Number(session?.lastOutputTokens || 0),
|
|
4369
3028
|
lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
|
|
4370
3029
|
lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
|
|
4371
3030
|
lastContextTokens,
|
|
4372
3031
|
totalInputTokens: Number(session?.totalInputTokens || 0),
|
|
3032
|
+
totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
|
|
4373
3033
|
totalOutputTokens: Number(session?.totalOutputTokens || 0),
|
|
4374
3034
|
totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
|
|
4375
3035
|
totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
|
|
@@ -4445,6 +3105,21 @@ function parsedProviderModelVersion(id) {
|
|
|
4445
3105
|
workflowRoutes: summarizeWorkflowRoutes(nextConfig),
|
|
4446
3106
|
};
|
|
4447
3107
|
},
|
|
3108
|
+
// Mark onboarding as done WITHOUT touching routes/agents/provider. Used by
|
|
3109
|
+
// the TUI "skip" (Esc) path so the wizard doesn't reappear next launch,
|
|
3110
|
+
// while leaving any existing config routes untouched.
|
|
3111
|
+
skipOnboarding() {
|
|
3112
|
+
const nextConfig = { ...config };
|
|
3113
|
+
nextConfig.onboarding = {
|
|
3114
|
+
...(nextConfig.onboarding || {}),
|
|
3115
|
+
completed: true,
|
|
3116
|
+
version: ONBOARDING_VERSION,
|
|
3117
|
+
completedAt: new Date().toISOString(),
|
|
3118
|
+
skipped: true,
|
|
3119
|
+
};
|
|
3120
|
+
saveConfigAndAdopt(nextConfig);
|
|
3121
|
+
return this.getOnboardingStatus();
|
|
3122
|
+
},
|
|
4448
3123
|
getAutoClear() {
|
|
4449
3124
|
return normalizeAutoClearConfig(config.autoClear);
|
|
4450
3125
|
},
|
|
@@ -4546,7 +3221,9 @@ function parsedProviderModelVersion(id) {
|
|
|
4546
3221
|
}
|
|
4547
3222
|
await channels.stop('settings-disabled', { waitForExit: false }).catch(() => {});
|
|
4548
3223
|
} else {
|
|
4549
|
-
|
|
3224
|
+
// Enabling channels in settings only boots the worker when this session
|
|
3225
|
+
// is in remote mode; otherwise the toggle just persists config.
|
|
3226
|
+
if (remoteEnabled) scheduleChannelStart(0);
|
|
4550
3227
|
}
|
|
4551
3228
|
invalidatePreSessionToolSurface();
|
|
4552
3229
|
return this.getChannelSettings();
|
|
@@ -4581,14 +3258,57 @@ function parsedProviderModelVersion(id) {
|
|
|
4581
3258
|
saveConfigAndAdopt({ ...config, autoClear: next });
|
|
4582
3259
|
return { ...normalizeAutoClearConfig(config.autoClear), label: formatDurationMs(normalizeAutoClearConfig(config.autoClear).idleMs) };
|
|
4583
3260
|
},
|
|
3261
|
+
getUpdateSettings() {
|
|
3262
|
+
return {
|
|
3263
|
+
autoUpdate: autoUpdateEnabled(),
|
|
3264
|
+
currentVersion: updateCheckState.currentVersion || localPackageVersion(),
|
|
3265
|
+
latestVersion: updateCheckState.latestVersion,
|
|
3266
|
+
updateAvailable: updateCheckState.updateAvailable,
|
|
3267
|
+
lastCheckedAt: updateCheckState.lastCheckedAt,
|
|
3268
|
+
};
|
|
3269
|
+
},
|
|
3270
|
+
setAutoUpdate(enabled) {
|
|
3271
|
+
saveConfigAndAdopt({
|
|
3272
|
+
...config,
|
|
3273
|
+
update: { ...(config.update || {}), auto: enabled === true },
|
|
3274
|
+
});
|
|
3275
|
+
return this.getUpdateSettings();
|
|
3276
|
+
},
|
|
3277
|
+
async checkForUpdate(options = {}) {
|
|
3278
|
+
await checkForUpdateInternal({ force: options?.force === true });
|
|
3279
|
+
return this.getUpdateSettings();
|
|
3280
|
+
},
|
|
3281
|
+
async runUpdateNow() {
|
|
3282
|
+
const state = await runUpdateNowInternal();
|
|
3283
|
+
return { ok: state.phase === 'installed', ...state };
|
|
3284
|
+
},
|
|
3285
|
+
getUpdateStatus() {
|
|
3286
|
+
return { ...updateProcessState };
|
|
3287
|
+
},
|
|
4584
3288
|
async completeOnboarding(payload = {}) {
|
|
4585
|
-
|
|
3289
|
+
// Only fall back to the live runtime route when the caller actually sent a
|
|
3290
|
+
// defaultRoute. The onboarding "partial save" path (Main left unset, only
|
|
3291
|
+
// Search/agent picks) omits defaultRoute entirely and must NOT persist the
|
|
3292
|
+
// current route as Main or recreate the session.
|
|
3293
|
+
const defaultRoute = hasOwn(payload, 'defaultRoute')
|
|
3294
|
+
? normalizeWorkflowRoute(payload.defaultRoute, route)
|
|
3295
|
+
: null;
|
|
4586
3296
|
const workflowInput = payload.workflowRoutes && typeof payload.workflowRoutes === 'object'
|
|
4587
3297
|
? payload.workflowRoutes
|
|
4588
3298
|
: {};
|
|
4589
3299
|
const nextConfig = { ...config };
|
|
3300
|
+
if (hasOwn(payload, 'defaultProvider')) {
|
|
3301
|
+
const requested = clean(payload.defaultProvider);
|
|
3302
|
+
if (requested) {
|
|
3303
|
+
if (!isKnownProvider(requested)) throw new Error(`unknown provider "${payload.defaultProvider}"`);
|
|
3304
|
+
nextConfig.defaultProvider = requested;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
4590
3307
|
let presets = Array.isArray(nextConfig.presets) ? nextConfig.presets.slice() : [];
|
|
4591
3308
|
const workflowRoutes = { ...(nextConfig.workflowRoutes || {}) };
|
|
3309
|
+
// Track slots actually written THIS call so maintenance mirroring never
|
|
3310
|
+
// clobbers an existing slot the payload did not touch.
|
|
3311
|
+
const touchedWorkflowSlots = new Set();
|
|
4592
3312
|
|
|
4593
3313
|
if (defaultRoute) {
|
|
4594
3314
|
presets = upsertWorkflowPreset(presets, 'lead', defaultRoute);
|
|
@@ -4601,18 +3321,51 @@ function parsedProviderModelVersion(id) {
|
|
|
4601
3321
|
if (!normalized) continue;
|
|
4602
3322
|
workflowRoutes[slot] = normalized;
|
|
4603
3323
|
presets = upsertWorkflowPreset(presets, slot, normalized);
|
|
3324
|
+
touchedWorkflowSlots.add(slot);
|
|
4604
3325
|
}
|
|
4605
3326
|
|
|
4606
3327
|
nextConfig.presets = presets;
|
|
4607
3328
|
nextConfig.workflowRoutes = workflowRoutes;
|
|
4608
|
-
// Maintenance slots store a direct {provider, model} route.
|
|
4609
|
-
//
|
|
4610
|
-
//
|
|
3329
|
+
// Maintenance slots store a direct {provider, model} route. Only mirror a
|
|
3330
|
+
// slot that was actually written this call (touchedWorkflowSlots); an
|
|
3331
|
+
// untouched slot keeps its existing maintenance value.
|
|
4611
3332
|
nextConfig.maintenance = {
|
|
4612
3333
|
...(nextConfig.maintenance || {}),
|
|
4613
|
-
...(
|
|
4614
|
-
...(
|
|
3334
|
+
...(touchedWorkflowSlots.has('explorer') ? { explore: normalizeWorkflowRoute(workflowRoutes.explorer) } : {}),
|
|
3335
|
+
...(touchedWorkflowSlots.has('memory') ? { memory: normalizeWorkflowRoute(workflowRoutes.memory) } : {}),
|
|
4615
3336
|
};
|
|
3337
|
+
// Per-agent onboarding routes (id → route) for the full FIXED_AGENT_SLOTS
|
|
3338
|
+
// roster. Mirrors setAgentRoute persistence: config.agents[id], an
|
|
3339
|
+
// agent-<id> preset, and (for workflow-backed agents) the workflowRoute +
|
|
3340
|
+
// maintenance slot. Agents omitted from the payload inherit defaultRoute.
|
|
3341
|
+
const agentInput = payload.agentRoutes && typeof payload.agentRoutes === 'object'
|
|
3342
|
+
? payload.agentRoutes
|
|
3343
|
+
: null;
|
|
3344
|
+
if (agentInput) {
|
|
3345
|
+
const nextAgents = { ...(nextConfig.agents || {}) };
|
|
3346
|
+
const nextMaintenance = { ...(nextConfig.maintenance || {}) };
|
|
3347
|
+
for (const agent of FIXED_AGENT_SLOTS) {
|
|
3348
|
+
// Persist ONLY agents with an explicit override in the payload. An
|
|
3349
|
+
// omitted agent is left untouched (existing value preserved, or
|
|
3350
|
+
// unwritten so the runtime dynamically follows the Main Model). Never
|
|
3351
|
+
// fall back to defaultRoute here — that would overwrite an agent the
|
|
3352
|
+
// user did not choose.
|
|
3353
|
+
const routeToSave = normalizeWorkflowRoute(agentInput[agent.id]);
|
|
3354
|
+
if (!routeToSave) continue;
|
|
3355
|
+
nextAgents[agent.id] = routeToSave;
|
|
3356
|
+
presets = upsertWorkflowPreset(presets, agentPresetSlot(agent.id), routeToSave);
|
|
3357
|
+
if (agent.workflowSlot) {
|
|
3358
|
+
workflowRoutes[agent.workflowSlot] = routeToSave;
|
|
3359
|
+
presets = upsertWorkflowPreset(presets, agent.workflowSlot, routeToSave);
|
|
3360
|
+
if (agent.id === 'explore') nextMaintenance.explore = routeToSave;
|
|
3361
|
+
if (agent.id === 'maintainer') nextMaintenance.memory = routeToSave;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
nextConfig.agents = nextAgents;
|
|
3365
|
+
nextConfig.presets = presets;
|
|
3366
|
+
nextConfig.workflowRoutes = workflowRoutes;
|
|
3367
|
+
nextConfig.maintenance = nextMaintenance;
|
|
3368
|
+
}
|
|
4616
3369
|
nextConfig.onboarding = {
|
|
4617
3370
|
...(nextConfig.onboarding || {}),
|
|
4618
3371
|
completed: true,
|
|
@@ -4620,6 +3373,14 @@ function parsedProviderModelVersion(id) {
|
|
|
4620
3373
|
completedAt: new Date().toISOString(),
|
|
4621
3374
|
};
|
|
4622
3375
|
|
|
3376
|
+
// Optional native web-search route. Only persisted when provided; mirrors
|
|
3377
|
+
// setSearchRoute's config write (skip provider capability validation here
|
|
3378
|
+
// since onboarding routes come from the vetted provider model list).
|
|
3379
|
+
if (payload.searchRoute) {
|
|
3380
|
+
const searchToSave = normalizeSearchRouteConfig(payload.searchRoute);
|
|
3381
|
+
if (searchToSave) nextConfig.searchRoute = searchToSave;
|
|
3382
|
+
}
|
|
3383
|
+
|
|
4623
3384
|
saveConfigAndAdopt(nextConfig);
|
|
4624
3385
|
if (defaultRoute) {
|
|
4625
3386
|
route = resolveRoute(config, { provider: defaultRoute.provider, model: defaultRoute.model, effort: defaultRoute.effort });
|
|
@@ -4629,11 +3390,24 @@ function parsedProviderModelVersion(id) {
|
|
|
4629
3390
|
return this.getOnboardingStatus();
|
|
4630
3391
|
},
|
|
4631
3392
|
getChannelSetup() {
|
|
3393
|
+
// Flush a pending debounced backend switch first so setup readers
|
|
3394
|
+
// (Settings → Channel Setting, remote toggles) never observe the
|
|
3395
|
+
// previous backend during the 150ms debounce window.
|
|
3396
|
+
try { flushBackendSave(); } catch {}
|
|
4632
3397
|
return channelSetup();
|
|
4633
3398
|
},
|
|
4634
3399
|
getChannelWorkerStatus() {
|
|
4635
3400
|
return channels.status();
|
|
4636
3401
|
},
|
|
3402
|
+
startRemote() {
|
|
3403
|
+
return startRemote();
|
|
3404
|
+
},
|
|
3405
|
+
stopRemote(reason) {
|
|
3406
|
+
return stopRemote(reason);
|
|
3407
|
+
},
|
|
3408
|
+
isRemoteEnabled() {
|
|
3409
|
+
return isRemoteEnabled();
|
|
3410
|
+
},
|
|
4637
3411
|
saveDiscordToken(token) {
|
|
4638
3412
|
const result = saveDiscordToken(token);
|
|
4639
3413
|
reloadChannelsSoon();
|
|
@@ -4644,6 +3418,33 @@ function parsedProviderModelVersion(id) {
|
|
|
4644
3418
|
reloadChannelsSoon();
|
|
4645
3419
|
return result;
|
|
4646
3420
|
},
|
|
3421
|
+
saveTelegramToken(token) {
|
|
3422
|
+
const result = saveTelegramToken(token);
|
|
3423
|
+
reloadChannelsSoon();
|
|
3424
|
+
return result;
|
|
3425
|
+
},
|
|
3426
|
+
forgetTelegramToken() {
|
|
3427
|
+
const result = forgetTelegramToken();
|
|
3428
|
+
reloadChannelsSoon();
|
|
3429
|
+
return result;
|
|
3430
|
+
},
|
|
3431
|
+
setBackend(name) {
|
|
3432
|
+
// Validate synchronously (same contract as before: bad input throws on
|
|
3433
|
+
// this call so the TUI's try/catch can react immediately). The actual
|
|
3434
|
+
// channels-section file read-modify-write is the hitch source on the
|
|
3435
|
+
// settings-toggle key handler, so defer it through the same debounce
|
|
3436
|
+
// pattern as saveConfigAndAdopt/flushConfigSave instead of writing to
|
|
3437
|
+
// disk synchronously inside the key handler.
|
|
3438
|
+
const value = String(name || '').trim();
|
|
3439
|
+
if (value !== 'discord' && value !== 'telegram') {
|
|
3440
|
+
throw new Error('backend must be discord or telegram');
|
|
3441
|
+
}
|
|
3442
|
+
pendingBackendName = value;
|
|
3443
|
+
if (backendSaveTimer) clearTimeout(backendSaveTimer);
|
|
3444
|
+
backendSaveTimer = setTimeout(flushBackendSave, CONFIG_SAVE_DEBOUNCE_MS);
|
|
3445
|
+
backendSaveTimer.unref?.();
|
|
3446
|
+
return { ok: true, backend: value };
|
|
3447
|
+
},
|
|
4647
3448
|
saveWebhookAuthtoken(token) {
|
|
4648
3449
|
const result = saveWebhookAuthtoken(token);
|
|
4649
3450
|
reloadChannelsSoon();
|
|
@@ -4756,6 +3557,12 @@ function parsedProviderModelVersion(id) {
|
|
|
4756
3557
|
invalidateProviderCaches();
|
|
4757
3558
|
return result;
|
|
4758
3559
|
},
|
|
3560
|
+
async loginOpenCodeGoUsage() {
|
|
3561
|
+
const result = await loginOpenCodeGoUsage(cfgMod);
|
|
3562
|
+
reloadFullConfig();
|
|
3563
|
+
invalidateProviderCaches();
|
|
3564
|
+
return result;
|
|
3565
|
+
},
|
|
4759
3566
|
setLocalProvider(providerId, opts) {
|
|
4760
3567
|
const result = setLocalProvider(cfgMod, providerId, opts);
|
|
4761
3568
|
reloadFullConfig();
|
|
@@ -4866,26 +3673,51 @@ function parsedProviderModelVersion(id) {
|
|
|
4866
3673
|
const names = before.styles.map((style) => style.label || style.id).join(', ') || 'Default';
|
|
4867
3674
|
throw new Error(`output style must be one of ${names}`);
|
|
4868
3675
|
}
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
}
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
3676
|
+
// Adopt in-memory immediately so same-tick readers see the new style;
|
|
3677
|
+
// persist off the key-handler tick. NOTE: the disk write goes through
|
|
3678
|
+
// the dedicated flushOutputStyleSave debounce (sharedCfgMod whole-root
|
|
3679
|
+
// RMW) — cfgMod.saveConfig only serializes agent-section fields and
|
|
3680
|
+
// would drop the top-level outputStyle key.
|
|
3681
|
+
const nextConfig = { ...config, outputStyle: selected.id };
|
|
3682
|
+
if (nextConfig.agent && typeof nextConfig.agent === 'object' && !Array.isArray(nextConfig.agent)) {
|
|
3683
|
+
const agent = { ...nextConfig.agent };
|
|
3684
|
+
delete agent.outputStyle;
|
|
3685
|
+
nextConfig.agent = agent;
|
|
3686
|
+
}
|
|
3687
|
+
adoptConfig(nextConfig);
|
|
3688
|
+
pendingOutputStyleId = selected.id;
|
|
3689
|
+
if (outputStyleSaveTimer) clearTimeout(outputStyleSaveTimer);
|
|
3690
|
+
outputStyleSaveTimer = setTimeout(flushOutputStyleSave, CONFIG_SAVE_DEBOUNCE_MS);
|
|
3691
|
+
outputStyleSaveTimer.unref?.();
|
|
3692
|
+
// Reuse the catalog already scanned above for `before` instead of a
|
|
3693
|
+
// second forced-fresh filesystem scan; refresh the status cache
|
|
3694
|
+
// in-memory (short TTL, same as getOutputStyleStatusCached) so reads
|
|
3695
|
+
// during the debounce window see the just-selected style.
|
|
3696
|
+
const freshStatus = { configured: selected.id, current: selected, styles: before.styles };
|
|
3697
|
+
outputStyleStatusCache = freshStatus;
|
|
3698
|
+
outputStyleStatusCacheAt = performance.now();
|
|
3699
|
+
outputStyleStatusCacheDir = resolve(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR);
|
|
4880
3700
|
const hasConversation = sessionHasConversationMessages(session);
|
|
4881
3701
|
let appliedToCurrentSession = !hasConversation;
|
|
4882
3702
|
if (session?.id && !hasConversation) {
|
|
4883
|
-
|
|
3703
|
+
const closedSessionId = session.id;
|
|
3704
|
+
mgr.closeSession(closedSessionId, 'cli-output-style-switch');
|
|
4884
3705
|
session = null;
|
|
4885
|
-
|
|
3706
|
+
// Defer the recreate so the output-style picker can repaint first;
|
|
3707
|
+
// any failure still surfaces via the existing notice path.
|
|
3708
|
+
setTimeout(() => {
|
|
3709
|
+
recreateCurrentSessionIfReady().catch((err) => {
|
|
3710
|
+
try {
|
|
3711
|
+
notifyFnForSession(closedSessionId)(
|
|
3712
|
+
`Failed to start a new session after output style change: ${err?.message || err}`,
|
|
3713
|
+
{ level: 'error' },
|
|
3714
|
+
);
|
|
3715
|
+
} catch {}
|
|
3716
|
+
});
|
|
3717
|
+
}, 0);
|
|
4886
3718
|
}
|
|
4887
3719
|
invalidateContextStatusCache();
|
|
4888
|
-
return { ...
|
|
3720
|
+
return { ...freshStatus, appliedToCurrentSession };
|
|
4889
3721
|
},
|
|
4890
3722
|
async setWorkflow(workflowId) {
|
|
4891
3723
|
const id = normalizeWorkflowId(workflowId, DEFAULT_WORKFLOW_ID);
|
|
@@ -4933,6 +3765,13 @@ function parsedProviderModelVersion(id) {
|
|
|
4933
3765
|
saveConfigAndAdopt(nextConfig);
|
|
4934
3766
|
return routeToSave;
|
|
4935
3767
|
},
|
|
3768
|
+
async setDefaultProvider(provider) {
|
|
3769
|
+
const requested = clean(provider);
|
|
3770
|
+
if (!requested) throw new Error('provider is required');
|
|
3771
|
+
if (!isKnownProvider(requested)) throw new Error(`unknown provider "${provider}"`);
|
|
3772
|
+
saveConfigAndAdopt({ ...config, defaultProvider: requested });
|
|
3773
|
+
return requested;
|
|
3774
|
+
},
|
|
4936
3775
|
async ask(prompt, options = {}) {
|
|
4937
3776
|
activeTurnCount += 1;
|
|
4938
3777
|
// Lazy code-graph prewarm: kick off the build ONCE, on the first real
|
|
@@ -4947,6 +3786,32 @@ function parsedProviderModelVersion(id) {
|
|
|
4947
3786
|
try {
|
|
4948
3787
|
await refreshSessionForCwdIfNeeded('cwd-change');
|
|
4949
3788
|
if (!session?.id) await createCurrentSession('turn');
|
|
3789
|
+
// Remote outbound: ensure a transcript writer bound to the current
|
|
3790
|
+
// session.id + cwd. The channel worker's OutputForwarder tails this
|
|
3791
|
+
// JSONL and pushes the surface view to Discord. Gated on remoteEnabled
|
|
3792
|
+
// so non-remote sessions write nothing.
|
|
3793
|
+
if (remoteEnabled) {
|
|
3794
|
+
const twKey = `${session.id}\u0000${currentCwd}`;
|
|
3795
|
+
// Reset per-turn dedup tracker so an identical answer in a later turn
|
|
3796
|
+
// is still written (the guard only prevents same-turn double-write).
|
|
3797
|
+
_lastAppendedAssistant = '';
|
|
3798
|
+
if (_twKey !== twKey) {
|
|
3799
|
+
try {
|
|
3800
|
+
_transcriptWriter = createTranscriptWriter({
|
|
3801
|
+
mixdogHome: mixdogHome(),
|
|
3802
|
+
sessionId: session.id,
|
|
3803
|
+
cwd: currentCwd,
|
|
3804
|
+
pid: process.pid,
|
|
3805
|
+
});
|
|
3806
|
+
_transcriptWriter.writeSessionRecord();
|
|
3807
|
+
_twKey = twKey;
|
|
3808
|
+
} catch (error) {
|
|
3809
|
+
process.stderr.write(`mixdog: transcript-writer: init failed: ${error?.message || error}\n`);
|
|
3810
|
+
_transcriptWriter = null;
|
|
3811
|
+
_twKey = '';
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
4950
3815
|
hooks.emit('turn:start', { sessionId: session.id, prompt, cwd: currentCwd });
|
|
4951
3816
|
// UserPromptSubmit: bridge to the standard hook bus. A hook FAILURE
|
|
4952
3817
|
// must not block the turn, but a genuine blocked===true MUST throw.
|
|
@@ -4975,6 +3840,14 @@ function parsedProviderModelVersion(id) {
|
|
|
4975
3840
|
name: call?.name || 'tool',
|
|
4976
3841
|
callId: call?.id || null,
|
|
4977
3842
|
});
|
|
3843
|
+
// Mirror each tool card the TUI shows into the transcript so the
|
|
3844
|
+
// forwarder renders the same one-line tool summary on Discord.
|
|
3845
|
+
// calls carry { name, arguments, id } (loop.mjs response.toolCalls);
|
|
3846
|
+
// some providers use `input` — accept either.
|
|
3847
|
+
if (remoteEnabled && _transcriptWriter) {
|
|
3848
|
+
try { _transcriptWriter.appendToolUse(call?.name, call?.input ?? call?.arguments); }
|
|
3849
|
+
catch (error) { process.stderr.write(`mixdog: transcript-writer: onToolCall failed: ${error?.message || error}\n`); }
|
|
3850
|
+
}
|
|
4978
3851
|
}
|
|
4979
3852
|
if (typeof options.onToolCall === 'function') {
|
|
4980
3853
|
return await options.onToolCall(iter, calls);
|
|
@@ -4986,9 +3859,40 @@ function parsedProviderModelVersion(id) {
|
|
|
4986
3859
|
{
|
|
4987
3860
|
onTextDelta: options.onTextDelta,
|
|
4988
3861
|
onReasoningDelta: options.onReasoningDelta,
|
|
4989
|
-
onAssistantText:
|
|
3862
|
+
onAssistantText: (text) => {
|
|
3863
|
+
// Capture the same assistant text the TUI renders (final answer +
|
|
3864
|
+
// mid-turn preamble). Feed the writer, then ALWAYS call through to
|
|
3865
|
+
// the caller's hook so terminal rendering is never affected.
|
|
3866
|
+
if (remoteEnabled && _transcriptWriter) {
|
|
3867
|
+
try {
|
|
3868
|
+
const value = typeof text === 'string' ? text : (text == null ? '' : String(text));
|
|
3869
|
+
if (value.trim()) {
|
|
3870
|
+
_transcriptWriter.appendAssistant(value);
|
|
3871
|
+
// Track the last line we wrote so the post-turn final
|
|
3872
|
+
// append below can skip an identical re-write (buffered
|
|
3873
|
+
// providers surface the final answer here AND in result).
|
|
3874
|
+
_lastAppendedAssistant = value;
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
catch (error) { process.stderr.write(`mixdog: transcript-writer: onAssistantText failed: ${error?.message || error}\n`); }
|
|
3878
|
+
}
|
|
3879
|
+
return options.onAssistantText?.(text);
|
|
3880
|
+
},
|
|
4990
3881
|
onUsageDelta: options.onUsageDelta,
|
|
4991
|
-
onToolResult:
|
|
3882
|
+
onToolResult: (message) => {
|
|
3883
|
+
// Surface Edit diffs only: the forwarder reads toolUseResult
|
|
3884
|
+
// {oldString,newString}. Other tool results carry no diff payload,
|
|
3885
|
+
// so we append only when those fields are present.
|
|
3886
|
+
if (remoteEnabled && _transcriptWriter) {
|
|
3887
|
+
try {
|
|
3888
|
+
const tur = message?.toolUseResult;
|
|
3889
|
+
if (tur && (tur.oldString != null || tur.newString != null)) {
|
|
3890
|
+
_transcriptWriter.appendToolResult({ oldString: tur.oldString ?? '', newString: tur.newString ?? '' });
|
|
3891
|
+
}
|
|
3892
|
+
} catch (error) { process.stderr.write(`mixdog: transcript-writer: onToolResult failed: ${error?.message || error}\n`); }
|
|
3893
|
+
}
|
|
3894
|
+
return options.onToolResult?.(message);
|
|
3895
|
+
},
|
|
4992
3896
|
onToolApproval: options.onToolApproval,
|
|
4993
3897
|
onCompactEvent: options.onCompactEvent,
|
|
4994
3898
|
onStageChange: options.onStageChange,
|
|
@@ -4999,6 +3903,24 @@ function parsedProviderModelVersion(id) {
|
|
|
4999
3903
|
},
|
|
5000
3904
|
);
|
|
5001
3905
|
session = mgr.getSession(session.id) || session;
|
|
3906
|
+
// Final assistant text: onAssistantText fires only for buffered
|
|
3907
|
+
// (non-streaming) providers and for mid-turn preamble. Streaming
|
|
3908
|
+
// providers deliver the final answer via onTextDelta (which we do NOT
|
|
3909
|
+
// tap, to avoid per-delta spam), so append the authoritative final
|
|
3910
|
+
// content here from result.content. To avoid double-writing the
|
|
3911
|
+
// buffered case, only append when it differs from the last assistant
|
|
3912
|
+
// line onAssistantText already wrote.
|
|
3913
|
+
if (remoteEnabled && _transcriptWriter) {
|
|
3914
|
+
try {
|
|
3915
|
+
const finalText = result?.content != null ? String(result.content) : '';
|
|
3916
|
+
if (finalText.trim() && finalText !== _lastAppendedAssistant) {
|
|
3917
|
+
_transcriptWriter.appendAssistant(finalText);
|
|
3918
|
+
_lastAppendedAssistant = finalText;
|
|
3919
|
+
}
|
|
3920
|
+
} catch (error) {
|
|
3921
|
+
process.stderr.write(`mixdog: transcript-writer: final append failed: ${error?.message || error}\n`);
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
5002
3924
|
hooks.emit('turn:end', { sessionId: session.id, elapsedMs: Date.now() - startedAt });
|
|
5003
3925
|
// Stop: bridge to the standard hook bus. Best-effort; ignore result,
|
|
5004
3926
|
// never throw.
|
|
@@ -5008,6 +3930,18 @@ function parsedProviderModelVersion(id) {
|
|
|
5008
3930
|
return { result, session };
|
|
5009
3931
|
} catch (error) {
|
|
5010
3932
|
hooks.emit('turn:error', { sessionId: session?.id || null, elapsedMs: Date.now() - startedAt, error: error?.message || String(error) });
|
|
3933
|
+
// StopFailure: bridge a turn error to the standard hook bus. Spec:
|
|
3934
|
+
// output + exit code ignored, so pure fire-and-forget. error_type is a
|
|
3935
|
+
// simple regex mapping from the error message (default 'unknown').
|
|
3936
|
+
try {
|
|
3937
|
+
const msg = String(error?.message || error || '').toLowerCase();
|
|
3938
|
+
const errorType = /rate.?limit|429|too many requests/.test(msg) ? 'rate_limit'
|
|
3939
|
+
: /overloaded|529/.test(msg) ? 'overloaded'
|
|
3940
|
+
: /authenticat|unauthorized|401|invalid.*api.?key/.test(msg) ? 'authentication_failed'
|
|
3941
|
+
: /server.?error|5\d\d|internal error/.test(msg) ? 'server_error'
|
|
3942
|
+
: 'unknown';
|
|
3943
|
+
void hooks.dispatch('StopFailure', hookCommonPayload({ session_id: session?.id || null, error_type: errorType }));
|
|
3944
|
+
} catch { /* best-effort: StopFailure hook must never break teardown */ }
|
|
5011
3945
|
throw error;
|
|
5012
3946
|
} finally {
|
|
5013
3947
|
activeTurnCount = Math.max(0, activeTurnCount - 1);
|
|
@@ -5034,7 +3968,14 @@ function parsedProviderModelVersion(id) {
|
|
|
5034
3968
|
if (activeTurnCount > 0) {
|
|
5035
3969
|
return { changed: false, reason: 'compact skipped: turn in progress' };
|
|
5036
3970
|
}
|
|
3971
|
+
// Manual compact bypasses loop.mjs, so its PreCompact/PostCompact never
|
|
3972
|
+
// fire here — dispatch them explicitly via the session-property hooks
|
|
3973
|
+
// (wired at session create). Best-effort; a hook must not block compaction.
|
|
3974
|
+
try { await session.preCompactHook?.({ trigger: 'manual' }); }
|
|
3975
|
+
catch { /* best-effort: PreCompact hook must never break manual compact */ }
|
|
5037
3976
|
const result = await mgr.compactSessionMessages(session.id);
|
|
3977
|
+
try { await session.postCompactHook?.({ trigger: 'manual' }); }
|
|
3978
|
+
catch { /* best-effort: PostCompact hook must never break manual compact */ }
|
|
5038
3979
|
session = mgr.getSession(session.id) || session;
|
|
5039
3980
|
if (options.recoverAgent === true) {
|
|
5040
3981
|
try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
|
|
@@ -5206,9 +4147,15 @@ function parsedProviderModelVersion(id) {
|
|
|
5206
4147
|
const removed = registryRemovePlugin(key, { dataDir });
|
|
5207
4148
|
const nextConfig = { ...config };
|
|
5208
4149
|
const serverName = pluginMcpServerName(plugin);
|
|
5209
|
-
|
|
4150
|
+
const prefix = `${serverName}--`;
|
|
4151
|
+
const hasMatch = nextConfig.mcpServers && Object.keys(nextConfig.mcpServers).some(
|
|
4152
|
+
(k) => k === serverName || k.startsWith(prefix)
|
|
4153
|
+
);
|
|
4154
|
+
if (hasMatch) {
|
|
5210
4155
|
const current = { ...nextConfig.mcpServers };
|
|
5211
|
-
|
|
4156
|
+
for (const k of Object.keys(current)) {
|
|
4157
|
+
if (k === serverName || k.startsWith(prefix)) delete current[k];
|
|
4158
|
+
}
|
|
5212
4159
|
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
5213
4160
|
await connectConfiguredMcp({ reset: true });
|
|
5214
4161
|
invalidatePreSessionToolSurface();
|
|
@@ -5221,22 +4168,50 @@ function parsedProviderModelVersion(id) {
|
|
|
5221
4168
|
const root = clean(plugin.root);
|
|
5222
4169
|
const script = clean(plugin.mcpScript);
|
|
5223
4170
|
if (!root || !script) throw new Error('plugin has no MCP script');
|
|
5224
|
-
const scriptPath = join(root, script);
|
|
5225
|
-
if (!existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${scriptPath}`);
|
|
5226
4171
|
const serverName = pluginMcpServerName(plugin);
|
|
5227
4172
|
const nextConfig = { ...config };
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
4173
|
+
if (script === '.mcp.json') {
|
|
4174
|
+
const mcpJsonPath = join(root, script);
|
|
4175
|
+
if (!existsSync(mcpJsonPath)) throw new Error(`plugin MCP manifest not found: ${mcpJsonPath}`);
|
|
4176
|
+
const manifest = readJsonSafe(mcpJsonPath) || {};
|
|
4177
|
+
const rawServers = manifest.mcpServers;
|
|
4178
|
+
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
4179
|
+
if (!isPlainObject(rawServers)) throw new Error(`plugin .mcp.json missing mcpServers object: ${mcpJsonPath}`);
|
|
4180
|
+
const keys = Object.keys(rawServers).filter((k) => isPlainObject(rawServers[k]));
|
|
4181
|
+
if (!keys.length) throw new Error(`plugin .mcp.json has no mcpServers: ${mcpJsonPath}`);
|
|
4182
|
+
const ownedPrefix = `${serverName}--`;
|
|
4183
|
+
const nextServers = {};
|
|
4184
|
+
for (const [k, v] of Object.entries(nextConfig.mcpServers || {})) {
|
|
4185
|
+
if (k === serverName || k.startsWith(ownedPrefix)) continue;
|
|
4186
|
+
nextServers[k] = v;
|
|
4187
|
+
}
|
|
4188
|
+
for (const serverKey of keys) {
|
|
4189
|
+
const cfg = normalizePluginMcpServerConfig(rawServers[serverKey], root);
|
|
4190
|
+
cfg.env = {
|
|
4191
|
+
...(cfg.env || {}),
|
|
5235
4192
|
MIXDOG_PLUGIN_ROOT: root,
|
|
5236
4193
|
MIXDOG_PLUGIN_DATA: join(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR, 'plugins', 'data', clean(plugin.id || plugin.name || serverName)),
|
|
4194
|
+
};
|
|
4195
|
+
const key = keys.length === 1 ? serverName : `${serverName}--${serverKey}`;
|
|
4196
|
+
nextServers[key] = cfg;
|
|
4197
|
+
}
|
|
4198
|
+
nextConfig.mcpServers = nextServers;
|
|
4199
|
+
} else {
|
|
4200
|
+
const scriptPath = join(root, script);
|
|
4201
|
+
if (!existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${scriptPath}`);
|
|
4202
|
+
nextConfig.mcpServers = {
|
|
4203
|
+
...(nextConfig.mcpServers || {}),
|
|
4204
|
+
[serverName]: {
|
|
4205
|
+
command: 'node',
|
|
4206
|
+
args: [scriptPath],
|
|
4207
|
+
cwd: root,
|
|
4208
|
+
env: {
|
|
4209
|
+
MIXDOG_PLUGIN_ROOT: root,
|
|
4210
|
+
MIXDOG_PLUGIN_DATA: join(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR, 'plugins', 'data', clean(plugin.id || plugin.name || serverName)),
|
|
4211
|
+
},
|
|
5237
4212
|
},
|
|
5238
|
-
}
|
|
5239
|
-
}
|
|
4213
|
+
};
|
|
4214
|
+
}
|
|
5240
4215
|
saveConfigAndAdopt(nextConfig);
|
|
5241
4216
|
const status = await connectConfiguredMcp({ reset: true });
|
|
5242
4217
|
invalidatePreSessionToolSurface();
|
|
@@ -5306,7 +4281,15 @@ function parsedProviderModelVersion(id) {
|
|
|
5306
4281
|
return result;
|
|
5307
4282
|
},
|
|
5308
4283
|
async setRoute(next, options = {}) {
|
|
5309
|
-
|
|
4284
|
+
// Model/provider changes take effect on the NEXT session only — never
|
|
4285
|
+
// rewrite a running session's provider/model in place. A live turn's
|
|
4286
|
+
// prompt cache (Anthropic/OpenAI/etc.) is provider-keyed; flipping
|
|
4287
|
+
// session.provider mid-conversation forces a full cache-miss rewrite
|
|
4288
|
+
// of the entire history on the very next turn (seen as a promptΔ
|
|
4289
|
+
// spike + cache_ratio=0% in session-bench). `route` (this closure
|
|
4290
|
+
// variable) still updates immediately so the NEXT createCurrentSession()
|
|
4291
|
+
// picks it up; only the currently-open session is left untouched.
|
|
4292
|
+
const applyToCurrentSession = options?.applyToCurrentSession === true;
|
|
5310
4293
|
const requested = { ...(next || {}) };
|
|
5311
4294
|
validateRequestedModelSelector(config, requested);
|
|
5312
4295
|
const providerExplicitlyRequested = clean(next?.provider) !== '';
|
|
@@ -5334,12 +4317,6 @@ function parsedProviderModelVersion(id) {
|
|
|
5334
4317
|
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
5335
4318
|
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
5336
4319
|
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
5337
|
-
if (!applyToCurrentSession) {
|
|
5338
|
-
persistLeadRoute(selectedRoute);
|
|
5339
|
-
refreshStatuslineUsageSnapshot(route);
|
|
5340
|
-
scheduleStatuslineUsageRefresh();
|
|
5341
|
-
return selectedRoute;
|
|
5342
|
-
}
|
|
5343
4320
|
const leadRoute = persistLeadRoute(selectedRoute);
|
|
5344
4321
|
route = resolveRoute(config, leadRoute
|
|
5345
4322
|
? { model: workflowPresetId('lead') }
|
|
@@ -5347,6 +4324,11 @@ function parsedProviderModelVersion(id) {
|
|
|
5347
4324
|
await refreshRouteEffort(modelMeta);
|
|
5348
4325
|
refreshStatuslineUsageSnapshot(route);
|
|
5349
4326
|
scheduleStatuslineUsageRefresh();
|
|
4327
|
+
if (!applyToCurrentSession) {
|
|
4328
|
+
// `route` is updated for the next session; the open session (and its
|
|
4329
|
+
// live provider/model/cache chain) is left alone on purpose.
|
|
4330
|
+
return route;
|
|
4331
|
+
}
|
|
5350
4332
|
if (session) {
|
|
5351
4333
|
const updated = mgr.updateSessionRoute?.(session.id, {
|
|
5352
4334
|
provider: route.provider,
|
|
@@ -5413,10 +4395,28 @@ function parsedProviderModelVersion(id) {
|
|
|
5413
4395
|
async close(reason = 'cli-exit', options = {}) {
|
|
5414
4396
|
const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
|
|
5415
4397
|
closeRequested = true;
|
|
4398
|
+
// SessionEnd: bridge teardown to the standard hook bus. reason mapped to
|
|
4399
|
+
// standard values ('clear'/'exit' where applicable, else 'other'). Short
|
|
4400
|
+
// await guard so a slow hook cannot wedge teardown; best-effort.
|
|
4401
|
+
try {
|
|
4402
|
+
const rl = String(reason || '').toLowerCase();
|
|
4403
|
+
const endReason = /clear/.test(rl) ? 'clear'
|
|
4404
|
+
: /exit|quit|cli-exit|shutdown|sigint|sigterm/.test(rl) ? 'exit'
|
|
4405
|
+
: 'other';
|
|
4406
|
+
if (session?.id) {
|
|
4407
|
+
await withTeardownDeadline(
|
|
4408
|
+
Promise.resolve(hooks.dispatch('SessionEnd', hookCommonPayload({ session_id: session.id, reason: endReason }))).catch(() => {}),
|
|
4409
|
+
300,
|
|
4410
|
+
undefined,
|
|
4411
|
+
);
|
|
4412
|
+
}
|
|
4413
|
+
} catch { /* best-effort: SessionEnd hook must never wedge teardown */ }
|
|
5416
4414
|
// Persist any change that is still sitting in the debounce window so a
|
|
5417
4415
|
// toggle made right before exit is not lost. Synchronous + best-effort:
|
|
5418
4416
|
// teardown must continue even if the final write fails.
|
|
5419
4417
|
try { flushConfigSave(); } catch {}
|
|
4418
|
+
try { flushBackendSave(); } catch {}
|
|
4419
|
+
try { flushOutputStyleSave(); } catch {}
|
|
5420
4420
|
if (channelStartTimer) {
|
|
5421
4421
|
clearTimeout(channelStartTimer);
|
|
5422
4422
|
channelStartTimer = null;
|
|
@@ -5433,6 +4433,10 @@ function parsedProviderModelVersion(id) {
|
|
|
5433
4433
|
clearTimeout(providerModelWarmupTimer);
|
|
5434
4434
|
providerModelWarmupTimer = null;
|
|
5435
4435
|
}
|
|
4436
|
+
if (modelCatalogWarmupTimer) {
|
|
4437
|
+
clearTimeout(modelCatalogWarmupTimer);
|
|
4438
|
+
modelCatalogWarmupTimer = null;
|
|
4439
|
+
}
|
|
5436
4440
|
if (codeGraphPrewarmTimer) {
|
|
5437
4441
|
clearTimeout(codeGraphPrewarmTimer);
|
|
5438
4442
|
codeGraphPrewarmTimer = null;
|
|
@@ -5511,8 +4515,8 @@ function parsedProviderModelVersion(id) {
|
|
|
5511
4515
|
if (owner && !['cli', 'user', 'mixdog', 'legacy'].includes(owner)) return null;
|
|
5512
4516
|
const sourceType = clean(s.sourceType || '').toLowerCase();
|
|
5513
4517
|
const sourceName = clean(s.sourceName || '').toLowerCase();
|
|
5514
|
-
const
|
|
5515
|
-
const leadish =
|
|
4518
|
+
const agent = clean(s.agent || '').toLowerCase();
|
|
4519
|
+
const leadish = agent === 'lead'
|
|
5516
4520
|
|| sourceType === 'lead'
|
|
5517
4521
|
|| (sourceType === 'cli' && (!sourceName || sourceName === 'main'))
|
|
5518
4522
|
|| (!sourceType && !sourceName && !isAgentOwner(owner));
|