mixdog 0.9.0 → 0.9.2
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 +10 -3
- 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/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- 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/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- 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/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- 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 +12 -4
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -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 +131 -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 +94 -16
- 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-oauth.mjs +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- 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/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
- 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.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- 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/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- 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-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 +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- 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 +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +187 -43
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- 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/child-spawn-gate.mjs +0 -6
- 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/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- 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 +146 -9
- 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 +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- 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 +0 -11
- 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 -647
- 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 +81 -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 +26 -27
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- 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 +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +154 -20
- 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/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -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,6 +32,7 @@ import {
|
|
|
25
32
|
PROVIDER_STATUS_TOOL,
|
|
26
33
|
beginOAuthProviderLogin,
|
|
27
34
|
forgetProviderAuth,
|
|
35
|
+
isKnownProvider,
|
|
28
36
|
loginOAuthProvider,
|
|
29
37
|
providerSetup,
|
|
30
38
|
renderProviderStatus,
|
|
@@ -35,7 +43,10 @@ import {
|
|
|
35
43
|
} from './standalone/provider-admin.mjs';
|
|
36
44
|
import { createUsageDashboard } from './standalone/usage-dashboard.mjs';
|
|
37
45
|
import { fetchOAuthUsageSnapshot } from './runtime/agent/orchestrator/providers/oauth-usage.mjs';
|
|
38
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
getModelMetadataSync,
|
|
48
|
+
warmCatalogsInBackground,
|
|
49
|
+
} from './runtime/agent/orchestrator/providers/model-catalog.mjs';
|
|
39
50
|
import {
|
|
40
51
|
isResponsesFreeformTool,
|
|
41
52
|
toResponsesCustomTool,
|
|
@@ -46,13 +57,16 @@ import {
|
|
|
46
57
|
deleteSchedule,
|
|
47
58
|
deleteWebhook,
|
|
48
59
|
forgetDiscordToken,
|
|
60
|
+
forgetTelegramToken,
|
|
49
61
|
forgetWebhookAuthtoken,
|
|
50
62
|
renderChannelStatus,
|
|
51
63
|
saveChannel,
|
|
52
64
|
saveDiscordToken,
|
|
65
|
+
saveTelegramToken,
|
|
53
66
|
saveSchedule,
|
|
54
67
|
saveWebhook,
|
|
55
68
|
saveWebhookAuthtoken,
|
|
69
|
+
setBackend,
|
|
56
70
|
setScheduleEnabled,
|
|
57
71
|
setWebhookEnabled,
|
|
58
72
|
setWebhookConfig,
|
|
@@ -69,74 +83,11 @@ import {
|
|
|
69
83
|
estimateRequestReserveTokens,
|
|
70
84
|
estimateTranscriptContextUsage,
|
|
71
85
|
estimateToolSchemaTokens,
|
|
86
|
+
resolveCompactBufferTokens,
|
|
87
|
+
resolveCompactTriggerTokens,
|
|
88
|
+
summarizeContextMessages,
|
|
72
89
|
} from './runtime/agent/orchestrator/session/context-utils.mjs';
|
|
73
90
|
|
|
74
|
-
// Default compaction buffer rules — kept in lockstep with the worker runtime
|
|
75
|
-
// (loop.mjs resolveCompactBufferRatio / manager.mjs compactBufferRatioForSession
|
|
76
|
-
// and compact.mjs compactionBufferTokensForBoundary). By default the trigger is
|
|
77
|
-
// the effective compact boundary itself; explicit buffer settings can lower it.
|
|
78
|
-
const CONTEXT_DEFAULT_BUFFER_RATIO = 0;
|
|
79
|
-
const CONTEXT_MAX_BUFFER_RATIO = 0.25;
|
|
80
|
-
const CONTEXT_LEGACY_DEFAULT_BUFFER_RATIO = 0.1;
|
|
81
|
-
function resolveContextBufferRatio(cfg = {}) {
|
|
82
|
-
// Percent-named inputs (bufferPercent/bufferPct/*_BUFFER_PERCENT): a value of
|
|
83
|
-
// 1 means 1% (→0.01). Ratio-named inputs (bufferRatio/bufferFraction): 0.01
|
|
84
|
-
// means 1%, while a value > 1 is treated as a legacy percent (10 → 10%).
|
|
85
|
-
const candidates = [
|
|
86
|
-
[cfg.bufferPercent, true],
|
|
87
|
-
[cfg.bufferPct, true],
|
|
88
|
-
[cfg.bufferRatio, false],
|
|
89
|
-
[cfg.bufferFraction, false],
|
|
90
|
-
[process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT, true],
|
|
91
|
-
[process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO, false],
|
|
92
|
-
];
|
|
93
|
-
for (const [raw, isPercent] of candidates) {
|
|
94
|
-
const n = Number(raw);
|
|
95
|
-
if (!Number.isFinite(n) || n <= 0) continue;
|
|
96
|
-
return isPercent ? Math.min(1, n / 100) : (n > 1 ? n / 100 : n);
|
|
97
|
-
}
|
|
98
|
-
return CONTEXT_DEFAULT_BUFFER_RATIO;
|
|
99
|
-
}
|
|
100
|
-
function isLegacyDefaultContextBufferTelemetry(cfg = {}, boundaryTokens = 0) {
|
|
101
|
-
const boundary = Number(boundaryTokens);
|
|
102
|
-
if (!Number.isFinite(boundary) || boundary <= 0) return false;
|
|
103
|
-
if (Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_TOKENS) > 0) return false;
|
|
104
|
-
for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
|
|
105
|
-
const n = Number(process.env[envName]);
|
|
106
|
-
if (Number.isFinite(n) && n > 0) return false;
|
|
107
|
-
}
|
|
108
|
-
for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
|
|
109
|
-
const n = Number(cfg?.[key]);
|
|
110
|
-
if (Number.isFinite(n) && n > 0) return false;
|
|
111
|
-
}
|
|
112
|
-
const explicitTokens = Number(cfg?.bufferTokens ?? cfg?.buffer);
|
|
113
|
-
const ratio = Number(cfg?.bufferRatio);
|
|
114
|
-
if (!Number.isFinite(explicitTokens) || explicitTokens <= 0 || !Number.isFinite(ratio) || Math.abs(ratio - CONTEXT_LEGACY_DEFAULT_BUFFER_RATIO) > 1e-9) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
const expectedTokens = Math.floor(boundary * CONTEXT_LEGACY_DEFAULT_BUFFER_RATIO);
|
|
118
|
-
const cfgBoundary = Number(cfg?.boundaryTokens);
|
|
119
|
-
const cfgTrigger = Number(cfg?.triggerTokens);
|
|
120
|
-
return Math.floor(explicitTokens) === expectedTokens
|
|
121
|
-
|| (Number.isFinite(cfgBoundary) && Math.floor(cfgBoundary) === Math.floor(boundary)
|
|
122
|
-
&& Number.isFinite(cfgTrigger) && cfgTrigger > 0
|
|
123
|
-
&& Math.floor(explicitTokens) === Math.max(0, Math.floor(boundary - cfgTrigger)));
|
|
124
|
-
}
|
|
125
|
-
function contextDefaultBufferTokens(boundaryTokens, cfg = {}) {
|
|
126
|
-
const boundary = Number(boundaryTokens);
|
|
127
|
-
if (!Number.isFinite(boundary) || boundary <= 0) return 0;
|
|
128
|
-
const effectiveCfg = isLegacyDefaultContextBufferTelemetry(cfg, boundary)
|
|
129
|
-
? { ...cfg, bufferTokens: null, buffer: null, bufferRatio: null }
|
|
130
|
-
: cfg;
|
|
131
|
-
const cap = Math.max(0, Math.floor(boundary * CONTEXT_MAX_BUFFER_RATIO));
|
|
132
|
-
const explicit = Number(effectiveCfg.bufferTokens ?? effectiveCfg.buffer);
|
|
133
|
-
if (Number.isFinite(explicit) && explicit > 0) {
|
|
134
|
-
return Math.min(Math.floor(explicit), cap);
|
|
135
|
-
}
|
|
136
|
-
const ratio = resolveContextBufferRatio(effectiveCfg);
|
|
137
|
-
return Math.max(0, Math.min(Math.floor(boundary * ratio), cap));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
91
|
function sessionMessageText(content) {
|
|
141
92
|
if (content == null) return '';
|
|
142
93
|
if (typeof content === 'string') return content;
|
|
@@ -153,10 +104,6 @@ function sessionMessageText(content) {
|
|
|
153
104
|
try { return JSON.stringify(content); } catch { return String(content); }
|
|
154
105
|
}
|
|
155
106
|
|
|
156
|
-
function roughTokenCount(text) {
|
|
157
|
-
return Math.ceil(String(text ?? '').length / 4);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
107
|
function messageContextText(message) {
|
|
161
108
|
if (!message || typeof message !== 'object') return '';
|
|
162
109
|
let text = sessionMessageText(message.content);
|
|
@@ -168,117 +115,6 @@ function messageContextText(message) {
|
|
|
168
115
|
return text;
|
|
169
116
|
}
|
|
170
117
|
|
|
171
|
-
function stripSystemReminder(text) {
|
|
172
|
-
return String(text || '')
|
|
173
|
-
.replace(/^\s*<system-reminder>\s*/i, '')
|
|
174
|
-
.replace(/\s*<\/system-reminder>\s*$/i, '')
|
|
175
|
-
.trim();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function splitMarkdownSections(text) {
|
|
179
|
-
const sections = [];
|
|
180
|
-
let current = [];
|
|
181
|
-
for (const line of String(text || '').split(/\r?\n/)) {
|
|
182
|
-
if (/^#\s+/.test(line) && current.length) {
|
|
183
|
-
const body = current.join('\n').trim();
|
|
184
|
-
if (body) sections.push(body);
|
|
185
|
-
current = [line];
|
|
186
|
-
} else {
|
|
187
|
-
current.push(line);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
const tail = current.join('\n').trim();
|
|
191
|
-
if (tail) sections.push(tail);
|
|
192
|
-
return sections;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function reminderSectionBucket(section) {
|
|
196
|
-
const heading = String(section.match(/^#\s+([^\n]+)/)?.[1] || '').trim().toLowerCase();
|
|
197
|
-
if (heading.includes('core memory')) return 'memory';
|
|
198
|
-
if (heading.includes('active workflow') || heading.includes('available agents') || heading.includes('workflow')) return 'workflow';
|
|
199
|
-
if (heading.includes('workspace')) return 'workspace';
|
|
200
|
-
if (heading.includes('environment')) return 'environment';
|
|
201
|
-
return 'other';
|
|
202
|
-
}
|
|
203
|
-
|
|
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
118
|
function isSessionPreviewNoise(text) {
|
|
283
119
|
const value = String(text || '').trim();
|
|
284
120
|
return !value
|
|
@@ -317,6 +153,14 @@ const STANDALONE_DATA_DIR = process.env.MIXDOG_DATA_DIR || join(MIXDOG_HOME, 'da
|
|
|
317
153
|
|
|
318
154
|
const DEFAULT_PROVIDER = 'anthropic-oauth';
|
|
319
155
|
const DEFAULT_MODEL = '';
|
|
156
|
+
|
|
157
|
+
// Resolve the provider to use when a route carries no explicit provider.
|
|
158
|
+
// Priority: config.defaultProvider (when it names a known provider) > DEFAULT_PROVIDER.
|
|
159
|
+
function resolveDefaultProvider(config) {
|
|
160
|
+
const configured = clean(config?.defaultProvider);
|
|
161
|
+
if (configured && isKnownProvider(configured)) return configured;
|
|
162
|
+
return DEFAULT_PROVIDER;
|
|
163
|
+
}
|
|
320
164
|
const TOOL_MODES = new Set(['full', 'readonly', 'lead']);
|
|
321
165
|
const ALL_EFFORT_LEVELS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
322
166
|
const EFFORT_LABELS = {
|
|
@@ -566,9 +410,9 @@ const READONLY_TOOL_NAMES = new Set([
|
|
|
566
410
|
'fetch',
|
|
567
411
|
'Skill',
|
|
568
412
|
]);
|
|
569
|
-
const AGENT_HIDDEN_WRAPPER_TOOLS = new Set([
|
|
413
|
+
const AGENT_HIDDEN_WRAPPER_TOOLS = new Set([]);
|
|
570
414
|
|
|
571
|
-
function
|
|
415
|
+
export function __applyStandaloneToolDefaultsForTest(tool) {
|
|
572
416
|
if (!tool || !AGENT_HIDDEN_WRAPPER_TOOLS.has(tool.name)) return tool;
|
|
573
417
|
return {
|
|
574
418
|
...tool,
|
|
@@ -578,6 +422,7 @@ function applyStandaloneToolDefaults(tool) {
|
|
|
578
422
|
},
|
|
579
423
|
};
|
|
580
424
|
}
|
|
425
|
+
const applyStandaloneToolDefaults = __applyStandaloneToolDefaultsForTest;
|
|
581
426
|
const DEFERRED_SELECT_ALIASES = {
|
|
582
427
|
filesystem: ['read', 'list', 'grep', 'find', 'glob'],
|
|
583
428
|
search: ['search', 'web_fetch'],
|
|
@@ -762,14 +607,18 @@ function modelMetaLooksResolved(meta) {
|
|
|
762
607
|
return Object.keys(meta).some((key) => key !== 'id' && key !== 'provider');
|
|
763
608
|
}
|
|
764
609
|
|
|
765
|
-
const OUTPUT_STYLE_ORDER = ['default', 'simple', '
|
|
610
|
+
const OUTPUT_STYLE_ORDER = ['default', 'simple', 'minimal', 'oneline'];
|
|
766
611
|
const OUTPUT_STYLE_ALIASES = new Map([
|
|
767
612
|
['compact', 'default'],
|
|
768
613
|
['normal', 'default'],
|
|
769
|
-
['extreme', '
|
|
770
|
-
['extremesimple', '
|
|
771
|
-
['extreme-simple', '
|
|
772
|
-
['extreme_simple', '
|
|
614
|
+
['extreme', 'minimal'],
|
|
615
|
+
['extremesimple', 'minimal'],
|
|
616
|
+
['extreme-simple', 'minimal'],
|
|
617
|
+
['extreme_simple', 'minimal'],
|
|
618
|
+
['mono', 'oneline'],
|
|
619
|
+
['oneline', 'oneline'],
|
|
620
|
+
['one-line', 'oneline'],
|
|
621
|
+
['one_line', 'oneline'],
|
|
773
622
|
]);
|
|
774
623
|
|
|
775
624
|
function normalizeOutputStyleId(value) {
|
|
@@ -892,6 +741,49 @@ function readJsonSafe(path) {
|
|
|
892
741
|
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
893
742
|
}
|
|
894
743
|
|
|
744
|
+
// Standard Claude Code project-local MCP ingress: read `.mcp.json` from the
|
|
745
|
+
// project root and return a cleaned { name: cfg } map. Best-effort — never
|
|
746
|
+
// throws. Accepts either the standard `{ mcpServers: {...} }` shape or a bare
|
|
747
|
+
// name->cfg map. Self-ref servers (`mixdog` / `trib-plugin`) are stripped for
|
|
748
|
+
// parity with loadConfig (which strips them from the persisted agent section
|
|
749
|
+
// but never sees `.mcp.json`). Inputs are not mutated.
|
|
750
|
+
function readProjectMcpServers(cwd) {
|
|
751
|
+
const path = join(cwd || '.', '.mcp.json');
|
|
752
|
+
if (!existsSync(path)) return {};
|
|
753
|
+
let raw;
|
|
754
|
+
try {
|
|
755
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
756
|
+
} catch (error) {
|
|
757
|
+
process.stderr.write(`[mcp-client] Ignoring unparseable .mcp.json at ${path}: ${error?.message || String(error)}\n`);
|
|
758
|
+
return {};
|
|
759
|
+
}
|
|
760
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
761
|
+
const map = raw.mcpServers && typeof raw.mcpServers === 'object' && !Array.isArray(raw.mcpServers)
|
|
762
|
+
? raw.mcpServers
|
|
763
|
+
: raw;
|
|
764
|
+
if (!map || typeof map !== 'object' || Array.isArray(map)) return {};
|
|
765
|
+
const out = {};
|
|
766
|
+
for (const [name, cfg] of Object.entries(map)) {
|
|
767
|
+
const key = clean(name);
|
|
768
|
+
if (!key) continue;
|
|
769
|
+
const lower = key.toLowerCase();
|
|
770
|
+
if (lower === 'mixdog' || lower === 'trib-plugin') continue;
|
|
771
|
+
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) continue;
|
|
772
|
+
// stdio entries (command + no url) spawn relative to the process launch
|
|
773
|
+
// dir, but mixdog tracks the project dir in memory (no process.chdir).
|
|
774
|
+
// Anchor their cwd to the .mcp.json directory: default when absent, resolve
|
|
775
|
+
// relative values against it, keep absolute values as-is. resolve(base, p)
|
|
776
|
+
// already returns `p` unchanged when it is absolute. Clone — never mutate.
|
|
777
|
+
const isStdio = typeof cfg.command === 'string' && cfg.command !== '' && !cfg.url;
|
|
778
|
+
if (isStdio) {
|
|
779
|
+
out[key] = { ...cfg, cwd: typeof cfg.cwd === 'string' && cfg.cwd ? resolve(cwd, cfg.cwd) : resolve(cwd) };
|
|
780
|
+
} else {
|
|
781
|
+
out[key] = cfg;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return out;
|
|
785
|
+
}
|
|
786
|
+
|
|
895
787
|
function countSkillFiles(root) {
|
|
896
788
|
const skillsDir = join(root, 'skills');
|
|
897
789
|
if (!existsSync(skillsDir)) return 0;
|
|
@@ -982,7 +874,7 @@ function resolveRoute(config, { provider, model, effort, fast } = {}) {
|
|
|
982
874
|
}
|
|
983
875
|
}
|
|
984
876
|
|
|
985
|
-
const p = explicitProvider ||
|
|
877
|
+
const p = explicitProvider || resolveDefaultProvider(config);
|
|
986
878
|
const m = explicitModel || DEFAULT_MODEL;
|
|
987
879
|
const saved = modelSettingsFor(config, p, m);
|
|
988
880
|
return {
|
|
@@ -1043,12 +935,9 @@ function normalizeSystemShellCommand(value) {
|
|
|
1043
935
|
function normalizeAutoClearConfig(value = {}) {
|
|
1044
936
|
const raw = value && typeof value === 'object' ? value : {};
|
|
1045
937
|
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
938
|
return {
|
|
1049
939
|
enabled: raw.enabled !== false,
|
|
1050
940
|
idleMs: Number.isFinite(idleMs) && idleMs > 0 ? Math.max(60_000, Math.round(idleMs)) : AUTO_CLEAR_DEFAULT_IDLE_MS,
|
|
1051
|
-
...(normalizedCompactType ? { compactType: normalizedCompactType } : {}),
|
|
1052
941
|
};
|
|
1053
942
|
}
|
|
1054
943
|
|
|
@@ -1238,7 +1127,12 @@ function routeForStatusline(route) {
|
|
|
1238
1127
|
const preset = route.preset || {};
|
|
1239
1128
|
if (preset.id) out.presetId = preset.id;
|
|
1240
1129
|
if (preset.name) out.presetName = preset.name;
|
|
1241
|
-
|
|
1130
|
+
// Prefer the preset's curated label, then the route's resolved model display
|
|
1131
|
+
// (set by refreshRouteEffort from the live/offline catalog). Without the
|
|
1132
|
+
// route fallback, a preset-less direct model (e.g. claude-fable-5) reaches
|
|
1133
|
+
// the statusline with no display and renders as the raw id.
|
|
1134
|
+
const modelDisplay = clean(preset.modelDisplay) || clean(route.modelDisplay);
|
|
1135
|
+
if (modelDisplay) out.modelDisplay = modelDisplay;
|
|
1242
1136
|
if (route.fast === true || route.fast === false) out.fast = route.fast;
|
|
1243
1137
|
else if (preset.fast === true || preset.fast === false) out.fast = preset.fast;
|
|
1244
1138
|
if (route.effectiveEffort) {
|
|
@@ -1374,22 +1268,27 @@ function agentSourceDirs(dataDir, id) {
|
|
|
1374
1268
|
];
|
|
1375
1269
|
}
|
|
1376
1270
|
|
|
1377
|
-
function readWorkflowPackFromDir(dir, source = 'built-in') {
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
const
|
|
1381
|
-
if (!id) return null;
|
|
1382
|
-
const entry = clean(manifest.entry) || 'WORKFLOW.md';
|
|
1383
|
-
const body = readTextSafe(join(dir, entry));
|
|
1271
|
+
function readWorkflowPackFromDir(dir, source = 'built-in', dirName = '') {
|
|
1272
|
+
const entry = 'WORKFLOW.md';
|
|
1273
|
+
const doc = readMarkdownDocument(readTextSafe(join(dir, entry)));
|
|
1274
|
+
const body = doc.body;
|
|
1384
1275
|
if (!body) return null;
|
|
1385
|
-
const
|
|
1276
|
+
const fm = doc.frontmatter || {};
|
|
1277
|
+
const id = normalizeWorkflowId(clean(fm.id) || dirName || basename(dir));
|
|
1278
|
+
if (!id) return null;
|
|
1279
|
+
const agentsConfigured = Object.prototype.hasOwnProperty.call(fm, 'agents');
|
|
1386
1280
|
return {
|
|
1387
1281
|
id,
|
|
1388
|
-
name: clean(
|
|
1389
|
-
description: clean(
|
|
1282
|
+
name: clean(fm.name) || id,
|
|
1283
|
+
description: clean(fm.description),
|
|
1390
1284
|
entry,
|
|
1391
1285
|
agentsConfigured,
|
|
1392
|
-
agents: agentsConfigured
|
|
1286
|
+
agents: agentsConfigured
|
|
1287
|
+
? String(fm.agents || '')
|
|
1288
|
+
.split(',')
|
|
1289
|
+
.map((agent) => normalizeAgentId(agent) || normalizeWorkflowId(agent))
|
|
1290
|
+
.filter(Boolean)
|
|
1291
|
+
: [],
|
|
1393
1292
|
body,
|
|
1394
1293
|
source,
|
|
1395
1294
|
};
|
|
@@ -1403,7 +1302,9 @@ function listWorkflowPacks(dataDir) {
|
|
|
1403
1302
|
try { entries = readdirSync(root, { withFileTypes: true }); } catch { entries = []; }
|
|
1404
1303
|
for (const entry of entries) {
|
|
1405
1304
|
if (!entry.isDirectory()) continue;
|
|
1406
|
-
const
|
|
1305
|
+
const dir = join(root, entry.name);
|
|
1306
|
+
if (!existsSync(join(dir, 'WORKFLOW.md'))) continue;
|
|
1307
|
+
const pack = readWorkflowPackFromDir(dir, source, entry.name);
|
|
1407
1308
|
if (pack) byId.set(pack.id, pack);
|
|
1408
1309
|
}
|
|
1409
1310
|
}
|
|
@@ -1421,10 +1322,10 @@ function activeWorkflowId(config) {
|
|
|
1421
1322
|
function loadWorkflowPack(dataDir, id) {
|
|
1422
1323
|
const wanted = normalizeWorkflowId(id, DEFAULT_WORKFLOW_ID);
|
|
1423
1324
|
for (const { root, source } of workflowSourceDirs(dataDir).reverse()) {
|
|
1424
|
-
const pack = readWorkflowPackFromDir(join(root, wanted), source);
|
|
1325
|
+
const pack = readWorkflowPackFromDir(join(root, wanted), source, wanted);
|
|
1425
1326
|
if (pack) return pack;
|
|
1426
1327
|
}
|
|
1427
|
-
return readWorkflowPackFromDir(join(STANDALONE_ROOT, 'workflows', DEFAULT_WORKFLOW_ID), 'built-in');
|
|
1328
|
+
return readWorkflowPackFromDir(join(STANDALONE_ROOT, 'workflows', DEFAULT_WORKFLOW_ID), 'built-in', DEFAULT_WORKFLOW_ID);
|
|
1428
1329
|
}
|
|
1429
1330
|
|
|
1430
1331
|
function workflowSummary(pack) {
|
|
@@ -1579,10 +1480,15 @@ function upsertWorkflowPreset(presets, slot, routeLike) {
|
|
|
1579
1480
|
|
|
1580
1481
|
function summarizeWorkflowRoutes(config) {
|
|
1581
1482
|
const routes = config?.workflowRoutes && typeof config.workflowRoutes === 'object' ? config.workflowRoutes : {};
|
|
1483
|
+
const fallbackProvider = resolveDefaultProvider(config);
|
|
1582
1484
|
const out = {};
|
|
1583
1485
|
for (const slot of WORKFLOW_ROUTE_SLOTS) {
|
|
1584
1486
|
const route = routes[slot];
|
|
1585
|
-
|
|
1487
|
+
// Read/interpret path: a route with a model but no provider falls back to
|
|
1488
|
+
// config.defaultProvider (then DEFAULT_PROVIDER).
|
|
1489
|
+
if (route?.model && (route?.provider || fallbackProvider)) {
|
|
1490
|
+
out[slot] = normalizeWorkflowRoute(route, { provider: fallbackProvider });
|
|
1491
|
+
}
|
|
1586
1492
|
}
|
|
1587
1493
|
return out;
|
|
1588
1494
|
}
|
|
@@ -1602,13 +1508,16 @@ function routeFromPreset(config, slotValue) {
|
|
|
1602
1508
|
function agentRouteFromConfig(config, agentId, _dataDir) {
|
|
1603
1509
|
const id = normalizeAgentId(agentId);
|
|
1604
1510
|
if (!id) return null;
|
|
1605
|
-
|
|
1606
|
-
|
|
1511
|
+
// Read/interpret path: inject config.defaultProvider (then DEFAULT_PROVIDER)
|
|
1512
|
+
// when a stored route omits its provider.
|
|
1513
|
+
const fallback = { provider: resolveDefaultProvider(config) };
|
|
1514
|
+
const explicit = normalizeWorkflowRoute(config?.agents?.[id], fallback)
|
|
1515
|
+
|| (id === 'maintainer' ? normalizeWorkflowRoute(config?.agents?.maintenance, fallback) : null);
|
|
1607
1516
|
if (explicit) return explicit;
|
|
1608
1517
|
|
|
1609
1518
|
const agent = FIXED_AGENT_SLOTS.find((item) => item.id === id);
|
|
1610
1519
|
if (agent?.workflowSlot) {
|
|
1611
|
-
const workflowRoute = normalizeWorkflowRoute(config?.workflowRoutes?.[agent.workflowSlot]);
|
|
1520
|
+
const workflowRoute = normalizeWorkflowRoute(config?.workflowRoutes?.[agent.workflowSlot], fallback);
|
|
1612
1521
|
if (workflowRoute) return workflowRoute;
|
|
1613
1522
|
}
|
|
1614
1523
|
|
|
@@ -2182,8 +2091,17 @@ export async function createMixdogSessionRuntime({
|
|
|
2182
2091
|
model,
|
|
2183
2092
|
cwd = process.cwd(),
|
|
2184
2093
|
toolMode = 'full',
|
|
2094
|
+
remote = false,
|
|
2185
2095
|
} = {}) {
|
|
2186
2096
|
bootProfile('session-runtime:start', { provider, model, toolMode, cwd });
|
|
2097
|
+
let remoteEnabled = remote === true;
|
|
2098
|
+
// Remote-mode transcript writer (Discord outbound). Lazily created per
|
|
2099
|
+
// session.id + cwd inside ask(); only active while remoteEnabled.
|
|
2100
|
+
let _transcriptWriter = null;
|
|
2101
|
+
let _twKey = '';
|
|
2102
|
+
// Last assistant text handed to the transcript writer (via onAssistantText),
|
|
2103
|
+
// so the post-turn final-content append can skip an exact duplicate.
|
|
2104
|
+
let _lastAppendedAssistant = '';
|
|
2187
2105
|
process.env.MIXDOG_QUIET_SESSION_LOG ??= '1';
|
|
2188
2106
|
const standaloneStartedAt = performance.now();
|
|
2189
2107
|
ensureStandaloneEnvironment({
|
|
@@ -2590,9 +2508,11 @@ export async function createMixdogSessionRuntime({
|
|
|
2590
2508
|
let sessionNeedsCwdRefresh = false;
|
|
2591
2509
|
let closeRequested = false;
|
|
2592
2510
|
let channelStartTimer = null;
|
|
2511
|
+
let channelStartPromise = null;
|
|
2593
2512
|
let providerSetupWarmupTimer = null;
|
|
2594
2513
|
let providerWarmupTimer = null;
|
|
2595
2514
|
let providerModelWarmupTimer = null;
|
|
2515
|
+
let modelCatalogWarmupTimer = null;
|
|
2596
2516
|
let codeGraphPrewarmTimer = null;
|
|
2597
2517
|
let codeGraphPrewarmInFlight = false;
|
|
2598
2518
|
let codeGraphPrewarmQueuedCwd = '';
|
|
@@ -2622,6 +2542,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2622
2542
|
}
|
|
2623
2543
|
const sessionPrewarmDelayMs = envDelayMs('MIXDOG_SESSION_PREWARM_DELAY_MS', 50, { min: 0, max: 10_000 });
|
|
2624
2544
|
const providerSetupWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_SETUP_WARMUP_DELAY_MS', 300, { min: 0, max: 60_000 });
|
|
2545
|
+
const modelCatalogWarmupDelayMs = envDelayMs('MIXDOG_MODEL_CATALOG_WARMUP_DELAY_MS', 200, { min: 0, max: 60_000 });
|
|
2625
2546
|
const providerWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_WARMUP_DELAY_MS', 1_500, { min: 0, max: 60_000 });
|
|
2626
2547
|
// Background model-catalog prefetch delay. Kept short so the first `/model`
|
|
2627
2548
|
// open finds a warm cache instead of paying a cold full network load. The
|
|
@@ -2655,6 +2576,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2655
2576
|
const modelPrefetchEnabled = !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
|
|
2656
2577
|
&& !envFlag('MIXDOG_DISABLE_MODEL_PREFETCH');
|
|
2657
2578
|
const codeGraphPrewarmEnabled = !envFlag('MIXDOG_DISABLE_CODE_GRAPH_PREWARM');
|
|
2579
|
+
const modelCatalogWarmupEnabled = !envFlag('MIXDOG_DISABLE_MODEL_CATALOG_WARMUP');
|
|
2658
2580
|
// Lazy code-graph prewarm (default ON): do NOT prewarm at startup / on cwd
|
|
2659
2581
|
// change — that fired ~250ms after the first frame and, in a large tree,
|
|
2660
2582
|
// burned a worker (and felt like a freeze) before the user did anything.
|
|
@@ -2667,8 +2589,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2667
2589
|
const notificationListeners = new Set();
|
|
2668
2590
|
let providerModelsCache = { models: null, at: 0 };
|
|
2669
2591
|
let providerModelsPromise = null;
|
|
2592
|
+
let providerModelsLoadSeq = 0;
|
|
2670
2593
|
let searchProviderModelsCache = { models: null, at: 0 };
|
|
2671
|
-
let searchProviderModelsPromise = null;
|
|
2672
2594
|
let usageDashboardCache = { dashboard: null, at: 0 };
|
|
2673
2595
|
let usageDashboardPromise = null;
|
|
2674
2596
|
let providerSetupCache = { setup: null, at: 0 };
|
|
@@ -2676,6 +2598,9 @@ export async function createMixdogSessionRuntime({
|
|
|
2676
2598
|
let providerSetupPromise = null;
|
|
2677
2599
|
let providerInitPromise = null;
|
|
2678
2600
|
let mcpFailures = [];
|
|
2601
|
+
let lastProjectMcpKey = null;
|
|
2602
|
+
let mcpConnectGeneration = 0;
|
|
2603
|
+
let mcpConnectInFlight = null;
|
|
2679
2604
|
let preSessionToolSurface = null;
|
|
2680
2605
|
let contextStatusCacheKey = null;
|
|
2681
2606
|
let contextStatusCacheValue = null;
|
|
@@ -2684,11 +2609,87 @@ export async function createMixdogSessionRuntime({
|
|
|
2684
2609
|
hooks.emit('runtime:start', { cwd: currentCwd, provider: route.provider, model: route.model, toolMode: mode });
|
|
2685
2610
|
bootProfile('hooks:ready', { ms: (performance.now() - hooksStartedAt).toFixed(1) });
|
|
2686
2611
|
|
|
2612
|
+
// ---------------------------------------------------------------------
|
|
2613
|
+
// Self-update (npm registry version check + optional auto-install).
|
|
2614
|
+
// updateCheckState mirrors the last-known checkLatestVersion() result;
|
|
2615
|
+
// updateProcessState tracks the in-flight install lifecycle so the TUI can
|
|
2616
|
+
// poll getUpdateStatus() instead of needing a push/event channel. Both are
|
|
2617
|
+
// purely in-memory (per runtime instance) — the on-disk cache lives inside
|
|
2618
|
+
// update-checker.mjs and is what actually enforces the 24h TTL.
|
|
2619
|
+
let updateCheckState = {
|
|
2620
|
+
currentVersion: null,
|
|
2621
|
+
latestVersion: null,
|
|
2622
|
+
updateAvailable: false,
|
|
2623
|
+
lastCheckedAt: 0,
|
|
2624
|
+
};
|
|
2625
|
+
// phase: 'idle' | 'checking' | 'installing' | 'installed' | 'failed'
|
|
2626
|
+
let updateProcessState = { phase: 'idle', version: null, error: null };
|
|
2627
|
+
|
|
2628
|
+
function autoUpdateEnabled() {
|
|
2629
|
+
return config?.update?.auto === true;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
async function checkForUpdateInternal({ force = false } = {}) {
|
|
2633
|
+
if (updateProcessState.phase !== 'installing') updateProcessState.phase = 'checking';
|
|
2634
|
+
try {
|
|
2635
|
+
const result = await checkLatestVersion({ force, dataDir: cfgMod.getPluginData?.() || STANDALONE_DATA_DIR });
|
|
2636
|
+
updateCheckState = {
|
|
2637
|
+
currentVersion: result.currentVersion,
|
|
2638
|
+
latestVersion: result.latestVersion,
|
|
2639
|
+
updateAvailable: result.updateAvailable,
|
|
2640
|
+
lastCheckedAt: result.lastCheckedAt,
|
|
2641
|
+
};
|
|
2642
|
+
} catch {
|
|
2643
|
+
// checkLatestVersion() is already silent-safe; this catch is belt-and-
|
|
2644
|
+
// braces so a boot-time call can never crash the runtime.
|
|
2645
|
+
} finally {
|
|
2646
|
+
if (updateProcessState.phase === 'checking') updateProcessState.phase = 'idle';
|
|
2647
|
+
}
|
|
2648
|
+
return updateCheckState;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
async function runUpdateNowInternal() {
|
|
2652
|
+
if (updateProcessState.phase === 'installing') {
|
|
2653
|
+
return { ...updateProcessState, alreadyInstalling: true, error: 'update already in progress' };
|
|
2654
|
+
}
|
|
2655
|
+
updateProcessState = { phase: 'installing', version: null, error: null };
|
|
2656
|
+
try {
|
|
2657
|
+
const result = await runGlobalUpdate();
|
|
2658
|
+
if (result?.ok) {
|
|
2659
|
+
updateProcessState = { phase: 'installed', version: result.version || null, error: null };
|
|
2660
|
+
} else {
|
|
2661
|
+
updateProcessState = { phase: 'failed', version: null, error: result?.error || 'update failed' };
|
|
2662
|
+
}
|
|
2663
|
+
} catch (err) {
|
|
2664
|
+
updateProcessState = { phase: 'failed', version: null, error: err?.message || String(err) };
|
|
2665
|
+
}
|
|
2666
|
+
return updateProcessState;
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
// Non-blocking boot hook: fires after the runtime object below is fully
|
|
2670
|
+
// constructed (setTimeout(0) defers past the synchronous return), so a
|
|
2671
|
+
// slow/hanging registry request or npm install can never delay session
|
|
2672
|
+
// boot. Auto-update defaults to OFF (config.update.auto is undefined until
|
|
2673
|
+
// the user opts in via setAutoUpdate(true)); a failed check or install is
|
|
2674
|
+
// swallowed — getUpdateStatus()/getUpdateSettings() are the only surfaces,
|
|
2675
|
+
// there is no push notice channel from runtime -> TUI today.
|
|
2676
|
+
const updateBootTimer = setTimeout(() => {
|
|
2677
|
+
void (async () => {
|
|
2678
|
+
await checkForUpdateInternal({ force: false });
|
|
2679
|
+
if (autoUpdateEnabled() && updateCheckState.updateAvailable) {
|
|
2680
|
+
await runUpdateNowInternal();
|
|
2681
|
+
}
|
|
2682
|
+
})().catch(() => {});
|
|
2683
|
+
}, 0);
|
|
2684
|
+
updateBootTimer.unref?.();
|
|
2685
|
+
|
|
2687
2686
|
function mcpTransportLabel(cfg = {}) {
|
|
2688
2687
|
if (cfg.autoDetect) return `autoDetect:${cfg.autoDetect}`;
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2688
|
+
try {
|
|
2689
|
+
return mcpClient.resolveMcpTransportKind(cfg);
|
|
2690
|
+
} catch {
|
|
2691
|
+
return 'unknown';
|
|
2692
|
+
}
|
|
2692
2693
|
}
|
|
2693
2694
|
|
|
2694
2695
|
function emitRuntimeNotification(content, meta = {}) {
|
|
@@ -2733,9 +2734,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2733
2734
|
}
|
|
2734
2735
|
|
|
2735
2736
|
function mcpStatus() {
|
|
2736
|
-
const
|
|
2737
|
-
? config.mcpServers
|
|
2738
|
-
: {};
|
|
2737
|
+
const { servers: configured, sources } = resolveEffectiveMcpServers();
|
|
2739
2738
|
const connected = new Map((mcpClient.getMcpServerStatus?.() || []).map((row) => [row.name, row]));
|
|
2740
2739
|
const failures = new Map((mcpFailures || []).map((row) => [row.name, row]));
|
|
2741
2740
|
const servers = [];
|
|
@@ -2752,6 +2751,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2752
2751
|
toolCount: live?.toolCount || 0,
|
|
2753
2752
|
tools: live?.tools || [],
|
|
2754
2753
|
error: fail?.msg || null,
|
|
2754
|
+
source: sources[name] || 'config',
|
|
2755
2755
|
});
|
|
2756
2756
|
connected.delete(name);
|
|
2757
2757
|
}
|
|
@@ -2856,7 +2856,6 @@ export async function createMixdogSessionRuntime({
|
|
|
2856
2856
|
// context (see composeSystemPrompt), so there is nothing prompt-side to
|
|
2857
2857
|
// refresh either. `markRefresh`/`changed` are kept only for signature
|
|
2858
2858
|
// compatibility with existing callers.
|
|
2859
|
-
void changed;
|
|
2860
2859
|
void markRefresh;
|
|
2861
2860
|
// Lazy mode: before the first turn (e.g. the initial project-selection
|
|
2862
2861
|
// cwd set), do NOT prewarm — that is exactly the post-first-frame freeze
|
|
@@ -2867,6 +2866,21 @@ export async function createMixdogSessionRuntime({
|
|
|
2867
2866
|
} else {
|
|
2868
2867
|
scheduleCodeGraphPrewarm(changed ? 0 : codeGraphPrewarmDelayMs, changed ? 'cwd-change' : 'cwd');
|
|
2869
2868
|
}
|
|
2869
|
+
// Project-local `.mcp.json` follows the cwd: when the effective project MCP
|
|
2870
|
+
// set changes, reconnect in the background (never await — this stays sync,
|
|
2871
|
+
// and the session is preserved). Guarded so a no-op cwd change does not
|
|
2872
|
+
// churn connections.
|
|
2873
|
+
if (changed) {
|
|
2874
|
+
try {
|
|
2875
|
+
const nextKey = resolved + '\u0000' + JSON.stringify(readProjectMcpServers(resolved));
|
|
2876
|
+
if (nextKey !== lastProjectMcpKey) {
|
|
2877
|
+
lastProjectMcpKey = nextKey;
|
|
2878
|
+
void connectConfiguredMcp({ reset: true })
|
|
2879
|
+
.then(() => invalidatePreSessionToolSurface())
|
|
2880
|
+
.catch(() => {});
|
|
2881
|
+
}
|
|
2882
|
+
} catch {}
|
|
2883
|
+
}
|
|
2870
2884
|
return currentCwd;
|
|
2871
2885
|
}
|
|
2872
2886
|
|
|
@@ -2880,17 +2894,23 @@ export async function createMixdogSessionRuntime({
|
|
|
2880
2894
|
}
|
|
2881
2895
|
|
|
2882
2896
|
function skillContent(name) {
|
|
2883
|
-
const
|
|
2884
|
-
? contextMod.
|
|
2897
|
+
const res = typeof contextMod.loadSkillResource === 'function'
|
|
2898
|
+
? contextMod.loadSkillResource(name, currentCwd)
|
|
2885
2899
|
: null;
|
|
2886
|
-
if (!
|
|
2887
|
-
return { name, content };
|
|
2900
|
+
if (!res) throw new Error(`skill not found: ${name}`);
|
|
2901
|
+
return { name, content: res.content, dir: res.dir };
|
|
2888
2902
|
}
|
|
2889
2903
|
|
|
2890
2904
|
function skillToolContent(name) {
|
|
2891
2905
|
const skill = skillContent(name);
|
|
2892
|
-
|
|
2893
|
-
|
|
2906
|
+
// Return the general tool envelope so the main/Lead session behaves the
|
|
2907
|
+
// same as agent-loop sessions: the model-visible tool_result is the short
|
|
2908
|
+
// stub (`Loaded skill: <name>`) and the full SKILL.md body is delivered
|
|
2909
|
+
// ONCE as a separate injected role:'user' message (newMessages). The
|
|
2910
|
+
// envelope passes through internal-tools._normalize untouched (it preserves
|
|
2911
|
+
// __toolEnvelope objects), and the agent loop's central normalizeToolEnvelope
|
|
2912
|
+
// splits it into stub + injected user body.
|
|
2913
|
+
return contextMod.buildSkillToolEnvelope(skill.name, skill.content, skill.dir);
|
|
2894
2914
|
}
|
|
2895
2915
|
|
|
2896
2916
|
function addProjectSkill(input = {}) {
|
|
@@ -2965,19 +2985,52 @@ export async function createMixdogSessionRuntime({
|
|
|
2965
2985
|
};
|
|
2966
2986
|
}
|
|
2967
2987
|
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2988
|
+
// Merge mixdog-config `agent.mcpServers` with project-local `.mcp.json`.
|
|
2989
|
+
// On name collision the mixdog-config entry WINS. `sources[name]` records
|
|
2990
|
+
// each server's origin ('config' | 'project') for status reporting.
|
|
2991
|
+
function resolveEffectiveMcpServers() {
|
|
2992
|
+
const configured = config?.mcpServers && typeof config.mcpServers === 'object'
|
|
2972
2993
|
? config.mcpServers
|
|
2973
2994
|
: {};
|
|
2974
|
-
|
|
2995
|
+
const project = readProjectMcpServers(currentCwd);
|
|
2996
|
+
const servers = { ...project, ...configured };
|
|
2997
|
+
const sources = {};
|
|
2998
|
+
for (const name of Object.keys(project)) sources[name] = 'project';
|
|
2999
|
+
for (const name of Object.keys(configured)) sources[name] = 'config';
|
|
3000
|
+
return { servers, sources };
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
async function connectConfiguredMcp({ reset = false } = {}) {
|
|
3004
|
+
// Serialize reconnects: boot connect, cwd-change reset, and rapid cwd
|
|
3005
|
+
// switches must never interleave their disconnect/connect phases, or an
|
|
3006
|
+
// older run finishing after a newer reset could re-add stale servers into
|
|
3007
|
+
// the shared client registry. Approach: a generation token + a single
|
|
3008
|
+
// in-flight promise. Each call bumps the generation, waits for any prior
|
|
3009
|
+
// run to finish, then bails if a newer call has superseded it — leaving the
|
|
3010
|
+
// latest requested effective-server-set in the registry.
|
|
3011
|
+
const gen = ++mcpConnectGeneration;
|
|
3012
|
+
if (mcpConnectInFlight) {
|
|
3013
|
+
try { await mcpConnectInFlight; } catch { /* prior run's failures already captured */ }
|
|
3014
|
+
}
|
|
3015
|
+
if (gen !== mcpConnectGeneration) return mcpStatus();
|
|
3016
|
+
const run = (async () => {
|
|
3017
|
+
if (reset) await mcpClient.disconnectAll?.();
|
|
3018
|
+
mcpFailures = [];
|
|
3019
|
+
const { servers } = resolveEffectiveMcpServers();
|
|
3020
|
+
if (Object.keys(servers).length === 0) return;
|
|
3021
|
+
try {
|
|
3022
|
+
await mcpClient.connectMcpServers(servers);
|
|
3023
|
+
} catch (error) {
|
|
3024
|
+
mcpFailures = Array.isArray(error?.failures)
|
|
3025
|
+
? error.failures
|
|
3026
|
+
: [{ name: 'mcp', msg: error?.message || String(error) }];
|
|
3027
|
+
}
|
|
3028
|
+
})();
|
|
3029
|
+
mcpConnectInFlight = run;
|
|
2975
3030
|
try {
|
|
2976
|
-
await
|
|
2977
|
-
}
|
|
2978
|
-
|
|
2979
|
-
? error.failures
|
|
2980
|
-
: [{ name: 'mcp', msg: error?.message || String(error) }];
|
|
3031
|
+
await run;
|
|
3032
|
+
} finally {
|
|
3033
|
+
if (mcpConnectInFlight === run) mcpConnectInFlight = null;
|
|
2981
3034
|
}
|
|
2982
3035
|
return mcpStatus();
|
|
2983
3036
|
}
|
|
@@ -2985,10 +3038,42 @@ export async function createMixdogSessionRuntime({
|
|
|
2985
3038
|
function normalizeMcpServerInput(input = {}) {
|
|
2986
3039
|
const name = clean(input.name).toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
2987
3040
|
if (!name) throw new Error('MCP server name is required');
|
|
3041
|
+
const coerceStringRecord = (value) => {
|
|
3042
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
3043
|
+
const out = {};
|
|
3044
|
+
for (const [key, val] of Object.entries(value)) {
|
|
3045
|
+
if (val === undefined || val === null) continue;
|
|
3046
|
+
out[String(key)] = String(val);
|
|
3047
|
+
}
|
|
3048
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
3049
|
+
};
|
|
3050
|
+
const withOptionalHeaders = (config) => {
|
|
3051
|
+
const headers = coerceStringRecord(input.headers);
|
|
3052
|
+
if (headers) config.headers = headers;
|
|
3053
|
+
return config;
|
|
3054
|
+
};
|
|
2988
3055
|
const url = clean(input.url);
|
|
3056
|
+
const type = clean(input.type).toLowerCase();
|
|
2989
3057
|
if (url) {
|
|
3058
|
+
if (type === 'sse') {
|
|
3059
|
+
if (!/^https?:\/\//i.test(url)) throw new Error('MCP URL must start with http:// or https://');
|
|
3060
|
+
return { name, config: withOptionalHeaders({ type: 'sse', url }) };
|
|
3061
|
+
}
|
|
3062
|
+
if (type === 'ws') {
|
|
3063
|
+
if (!/^(?:wss?|https?):\/\//i.test(url)) {
|
|
3064
|
+
throw new Error('MCP WebSocket URL must start with ws://, wss://, http://, or https://');
|
|
3065
|
+
}
|
|
3066
|
+
return { name, config: withOptionalHeaders({ type: 'ws', url }) };
|
|
3067
|
+
}
|
|
3068
|
+
if (type === 'http' || type === 'streamable-http') {
|
|
3069
|
+
if (!/^https?:\/\//i.test(url)) throw new Error('MCP URL must start with http:// or https://');
|
|
3070
|
+
return { name, config: withOptionalHeaders({ type: 'http', url }) };
|
|
3071
|
+
}
|
|
3072
|
+
if (/^wss?:\/\//i.test(url)) {
|
|
3073
|
+
return { name, config: withOptionalHeaders({ type: 'ws', url }) };
|
|
3074
|
+
}
|
|
2990
3075
|
if (!/^https?:\/\//i.test(url)) throw new Error('MCP URL must start with http:// or https://');
|
|
2991
|
-
return { name, config: {
|
|
3076
|
+
return { name, config: withOptionalHeaders({ type: 'http', url }) };
|
|
2992
3077
|
}
|
|
2993
3078
|
const command = clean(input.command);
|
|
2994
3079
|
if (!command) throw new Error('MCP server command or URL is required');
|
|
@@ -3002,7 +3087,10 @@ export async function createMixdogSessionRuntime({
|
|
|
3002
3087
|
if (resolvedCwd !== root && !resolvedCwd.startsWith(`${root}\\`) && !resolvedCwd.startsWith(`${root}/`)) {
|
|
3003
3088
|
throw new Error('MCP server cwd must stay under the current project');
|
|
3004
3089
|
}
|
|
3005
|
-
|
|
3090
|
+
const config = { type: 'stdio', command, args, cwd: resolvedCwd };
|
|
3091
|
+
const env = coerceStringRecord(input.env);
|
|
3092
|
+
if (env) config.env = env;
|
|
3093
|
+
return { name, config };
|
|
3006
3094
|
}
|
|
3007
3095
|
|
|
3008
3096
|
const agentToolStartedAt = performance.now();
|
|
@@ -3036,10 +3124,12 @@ export async function createMixdogSessionRuntime({
|
|
|
3036
3124
|
if (msg?.method !== 'notifications/claude/channel') return;
|
|
3037
3125
|
const params = msg?.params && typeof msg.params === 'object' ? msg.params : {};
|
|
3038
3126
|
const meta = params.meta && typeof params.meta === 'object' ? params.meta : {};
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
const
|
|
3042
|
-
|
|
3127
|
+
const content = channelNotificationModelContent(params);
|
|
3128
|
+
if (!content) return;
|
|
3129
|
+
const handled = emitRuntimeNotification(content, meta);
|
|
3130
|
+
if (!handled && session?.id && shouldMirrorChannelNotificationToPending(meta)) {
|
|
3131
|
+
try { mgr.enqueuePendingMessage(session.id, content); } catch {}
|
|
3132
|
+
}
|
|
3043
3133
|
},
|
|
3044
3134
|
});
|
|
3045
3135
|
bootProfile('channels:worker-ready', { ms: (performance.now() - channelsStartedAt).toFixed(1) });
|
|
@@ -3170,6 +3260,7 @@ export async function createMixdogSessionRuntime({
|
|
|
3170
3260
|
},
|
|
3171
3261
|
});
|
|
3172
3262
|
internalTools.markBootReady?.();
|
|
3263
|
+
try { lastProjectMcpKey = resolve(currentCwd) + '\u0000' + JSON.stringify(readProjectMcpServers(currentCwd)); } catch { lastProjectMcpKey = null; }
|
|
3173
3264
|
void connectConfiguredMcp()
|
|
3174
3265
|
.then((status) => bootProfile('mcp:ready', {
|
|
3175
3266
|
connected: Number(status?.connectedCount || 0),
|
|
@@ -3184,8 +3275,8 @@ export async function createMixdogSessionRuntime({
|
|
|
3184
3275
|
function invalidateProviderCaches() {
|
|
3185
3276
|
providerModelsCache = { models: null, at: 0 };
|
|
3186
3277
|
providerModelsPromise = null;
|
|
3278
|
+
providerModelsLoadSeq += 1;
|
|
3187
3279
|
searchProviderModelsCache = { models: null, at: 0 };
|
|
3188
|
-
searchProviderModelsPromise = null;
|
|
3189
3280
|
usageDashboardCache = { dashboard: null, at: 0 };
|
|
3190
3281
|
usageDashboardPromise = null;
|
|
3191
3282
|
providerSetupCache = { setup: null, at: 0 };
|
|
@@ -3632,17 +3723,11 @@ function parsedProviderModelVersion(id) {
|
|
|
3632
3723
|
searchProviderModelsCache = { models: rows, at: Date.now() };
|
|
3633
3724
|
return providerModelsFromCacheRows(rows);
|
|
3634
3725
|
}
|
|
3635
|
-
if (
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
return models;
|
|
3640
|
-
})
|
|
3641
|
-
.finally(() => {
|
|
3642
|
-
searchProviderModelsPromise = null;
|
|
3643
|
-
});
|
|
3726
|
+
if (force) {
|
|
3727
|
+
const models = await loadSearchProviderModelsFresh({ forceRefresh: true });
|
|
3728
|
+
searchProviderModelsCache = { models, at: Date.now() };
|
|
3729
|
+
return providerModelsFromCacheRows(models);
|
|
3644
3730
|
}
|
|
3645
|
-
return providerModelsFromCacheRows(await searchProviderModelsPromise);
|
|
3646
3731
|
}
|
|
3647
3732
|
|
|
3648
3733
|
function enabledSearchProviderConfig() {
|
|
@@ -3656,7 +3741,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3656
3741
|
return out;
|
|
3657
3742
|
}
|
|
3658
3743
|
|
|
3659
|
-
async function loadSearchProviderModelsFresh() {
|
|
3744
|
+
async function loadSearchProviderModelsFresh({ forceRefresh = false } = {}) {
|
|
3660
3745
|
const searchProviders = enabledSearchProviderConfig();
|
|
3661
3746
|
const providerNames = Object.keys(searchProviders);
|
|
3662
3747
|
if (!providerNames.length) return [];
|
|
@@ -3665,7 +3750,13 @@ function parsedProviderModelVersion(id) {
|
|
|
3665
3750
|
const provider = reg.getProvider(name);
|
|
3666
3751
|
if (typeof provider?.listModels !== 'function') return [];
|
|
3667
3752
|
try {
|
|
3668
|
-
|
|
3753
|
+
let models = null;
|
|
3754
|
+
if (forceRefresh && typeof provider._refreshModelCache === 'function') {
|
|
3755
|
+
models = await provider._refreshModelCache();
|
|
3756
|
+
}
|
|
3757
|
+
if (!Array.isArray(models)) {
|
|
3758
|
+
models = await provider.listModels();
|
|
3759
|
+
}
|
|
3669
3760
|
if (!Array.isArray(models)) return [];
|
|
3670
3761
|
const rows = [];
|
|
3671
3762
|
for (const m of models) {
|
|
@@ -3699,14 +3790,20 @@ function parsedProviderModelVersion(id) {
|
|
|
3699
3790
|
return results;
|
|
3700
3791
|
}
|
|
3701
3792
|
|
|
3702
|
-
async function loadProviderModelsFresh() {
|
|
3793
|
+
async function loadProviderModelsFresh({ forceRefresh = false } = {}) {
|
|
3703
3794
|
ensureFullConfig();
|
|
3704
3795
|
await ensureProvidersReady(config.providers || {});
|
|
3705
3796
|
const allProviders = [...reg.getAllProviders()];
|
|
3706
3797
|
const providerResults = await Promise.all(allProviders.map(async ([name, provider]) => {
|
|
3707
3798
|
if (typeof provider?.listModels !== 'function') return [];
|
|
3708
3799
|
try {
|
|
3709
|
-
|
|
3800
|
+
let models = null;
|
|
3801
|
+
if (forceRefresh && typeof provider._refreshModelCache === 'function') {
|
|
3802
|
+
models = await provider._refreshModelCache();
|
|
3803
|
+
}
|
|
3804
|
+
if (!Array.isArray(models)) {
|
|
3805
|
+
models = await provider.listModels();
|
|
3806
|
+
}
|
|
3710
3807
|
if (!Array.isArray(models)) return [];
|
|
3711
3808
|
const rows = [];
|
|
3712
3809
|
for (const m of models) {
|
|
@@ -3741,10 +3838,17 @@ function parsedProviderModelVersion(id) {
|
|
|
3741
3838
|
warmProviderModelCache();
|
|
3742
3839
|
return quickProviderModelRows();
|
|
3743
3840
|
}
|
|
3841
|
+
if (force) {
|
|
3842
|
+
const seq = ++providerModelsLoadSeq;
|
|
3843
|
+
const models = await loadProviderModelsFresh({ forceRefresh: true });
|
|
3844
|
+
if (seq === providerModelsLoadSeq) providerModelsCache = { models, at: Date.now() };
|
|
3845
|
+
return providerModelsFromCacheRows(models);
|
|
3846
|
+
}
|
|
3744
3847
|
if (!providerModelsPromise) {
|
|
3848
|
+
const seq = ++providerModelsLoadSeq;
|
|
3745
3849
|
providerModelsPromise = loadProviderModelsFresh()
|
|
3746
3850
|
.then((models) => {
|
|
3747
|
-
providerModelsCache = { models, at: Date.now() };
|
|
3851
|
+
if (seq === providerModelsLoadSeq) providerModelsCache = { models, at: Date.now() };
|
|
3748
3852
|
return models;
|
|
3749
3853
|
})
|
|
3750
3854
|
.finally(() => {
|
|
@@ -3756,9 +3860,10 @@ function parsedProviderModelVersion(id) {
|
|
|
3756
3860
|
|
|
3757
3861
|
function warmProviderModelCache() {
|
|
3758
3862
|
if (Array.isArray(providerModelsCache.models) || providerModelsPromise) return providerModelsPromise;
|
|
3863
|
+
const seq = ++providerModelsLoadSeq;
|
|
3759
3864
|
providerModelsPromise = loadProviderModelsFresh()
|
|
3760
3865
|
.then((models) => {
|
|
3761
|
-
providerModelsCache = { models, at: Date.now() };
|
|
3866
|
+
if (seq === providerModelsLoadSeq) providerModelsCache = { models, at: Date.now() };
|
|
3762
3867
|
bootProfile('provider-models:warm-ready', { count: models.length });
|
|
3763
3868
|
return models;
|
|
3764
3869
|
})
|
|
@@ -3795,12 +3900,22 @@ function parsedProviderModelVersion(id) {
|
|
|
3795
3900
|
const requested = hasOwn(route, 'effort') ? route.effort : (route.preset?.effort || null);
|
|
3796
3901
|
const effectiveEffort = coerceEffortFor(route.provider, modelMeta, requested);
|
|
3797
3902
|
const fastCapable = fastCapableFor(route.provider, modelMeta);
|
|
3903
|
+
// Carry the catalog display name onto the route so the statusline shows a
|
|
3904
|
+
// human label (e.g. "Claude Fable 5") for preset-less direct models instead
|
|
3905
|
+
// of the raw id. `name` is only trusted when it differs from the raw model
|
|
3906
|
+
// id (some providers echo the id as `name`), so it can't clobber a better
|
|
3907
|
+
// already-resolved label. Falls back to existing route.modelDisplay, then unset.
|
|
3908
|
+
const metaName = clean(modelMeta?.name);
|
|
3909
|
+
const modelDisplay = clean(modelMeta?.display) || clean(modelMeta?.displayName)
|
|
3910
|
+
|| (metaName && metaName !== clean(route.model) ? metaName : '')
|
|
3911
|
+
|| clean(route.modelDisplay);
|
|
3798
3912
|
route = {
|
|
3799
3913
|
...route,
|
|
3800
3914
|
fast: fastCapable ? route.fast === true : false,
|
|
3801
3915
|
fastCapable,
|
|
3802
3916
|
effectiveEffort,
|
|
3803
3917
|
effortOptions: effortItemsFor(route.provider, modelMeta, effectiveEffort),
|
|
3918
|
+
...(modelDisplay ? { modelDisplay } : {}),
|
|
3804
3919
|
};
|
|
3805
3920
|
return route;
|
|
3806
3921
|
}
|
|
@@ -3858,7 +3973,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3858
3973
|
preset: route.preset || undefined,
|
|
3859
3974
|
tools: toolSpecForMode(mode),
|
|
3860
3975
|
owner: 'cli',
|
|
3861
|
-
|
|
3976
|
+
agent: 'lead',
|
|
3862
3977
|
lane: 'cli',
|
|
3863
3978
|
sourceType: 'lead',
|
|
3864
3979
|
sourceName: 'main',
|
|
@@ -4022,6 +4137,27 @@ function parsedProviderModelVersion(id) {
|
|
|
4022
4137
|
providerModelWarmupTimer.unref?.();
|
|
4023
4138
|
}
|
|
4024
4139
|
|
|
4140
|
+
function scheduleModelCatalogWarmup(delayMs = modelCatalogWarmupDelayMs) {
|
|
4141
|
+
if (!modelCatalogWarmupEnabled) {
|
|
4142
|
+
bootProfile('model-catalog:warm-skipped', { reason: 'disabled' });
|
|
4143
|
+
return;
|
|
4144
|
+
}
|
|
4145
|
+
if (modelCatalogWarmupTimer || closeRequested) return;
|
|
4146
|
+
modelCatalogWarmupTimer = setTimeout(() => {
|
|
4147
|
+
modelCatalogWarmupTimer = null;
|
|
4148
|
+
if (closeRequested) return;
|
|
4149
|
+
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
4150
|
+
bootProfile('model-catalog:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
4151
|
+
scheduleModelCatalogWarmup(backgroundBusyRetryMs);
|
|
4152
|
+
return;
|
|
4153
|
+
}
|
|
4154
|
+
void warmCatalogsInBackground()
|
|
4155
|
+
.then(() => bootProfile('model-catalog:warm-ready'))
|
|
4156
|
+
.catch((error) => bootProfile('model-catalog:warm-failed', { error: error?.message || String(error) }));
|
|
4157
|
+
}, delayMs);
|
|
4158
|
+
modelCatalogWarmupTimer.unref?.();
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4025
4161
|
function scheduleStatuslineUsageWarmup(delayMs = statuslineUsageWarmupDelayMs) {
|
|
4026
4162
|
const providerId = clean(route?.provider);
|
|
4027
4163
|
if (!providerId || !providerId.includes('oauth')) {
|
|
@@ -4082,6 +4218,22 @@ function parsedProviderModelVersion(id) {
|
|
|
4082
4218
|
statuslineUsageRefreshTimer.unref?.();
|
|
4083
4219
|
}
|
|
4084
4220
|
|
|
4221
|
+
function invokeChannelStart() {
|
|
4222
|
+
if (channelStartPromise) return channelStartPromise;
|
|
4223
|
+
const startedAt = performance.now();
|
|
4224
|
+
bootProfile('channels:start:begin');
|
|
4225
|
+
channelStartPromise = channels.start()
|
|
4226
|
+
.then(() => bootProfile('channels:start:ready', { ms: (performance.now() - startedAt).toFixed(1) }))
|
|
4227
|
+
.catch((error) => bootProfile('channels:start:failed', {
|
|
4228
|
+
ms: (performance.now() - startedAt).toFixed(1),
|
|
4229
|
+
error: error?.message || String(error),
|
|
4230
|
+
}))
|
|
4231
|
+
.finally(() => {
|
|
4232
|
+
channelStartPromise = null;
|
|
4233
|
+
});
|
|
4234
|
+
return channelStartPromise;
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4085
4237
|
function scheduleChannelStart(delayMs = channelStartDelayMs) {
|
|
4086
4238
|
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
4087
4239
|
bootProfile('channels:start-skipped');
|
|
@@ -4091,28 +4243,59 @@ function parsedProviderModelVersion(id) {
|
|
|
4091
4243
|
bootProfile('channels:start-disabled');
|
|
4092
4244
|
return;
|
|
4093
4245
|
}
|
|
4094
|
-
if (channelStartTimer || closeRequested) return;
|
|
4246
|
+
if (channelStartTimer || channelStartPromise || closeRequested) return;
|
|
4095
4247
|
bootProfile('channels:start-scheduled', { delayMs });
|
|
4096
4248
|
channelStartTimer = setTimeout(() => {
|
|
4097
4249
|
channelStartTimer = null;
|
|
4098
4250
|
if (closeRequested) return;
|
|
4251
|
+
// A deferred start may straddle a stopRemote(); re-check before booting so
|
|
4252
|
+
// a turned-off session neither starts channels nor keeps rescheduling.
|
|
4253
|
+
if (!remoteEnabled) return;
|
|
4099
4254
|
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
4100
4255
|
bootProfile('channels:start-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
4101
4256
|
scheduleChannelStart(backgroundBusyRetryMs);
|
|
4102
4257
|
return;
|
|
4103
4258
|
}
|
|
4104
|
-
|
|
4105
|
-
bootProfile('channels:start:begin');
|
|
4106
|
-
channels.start()
|
|
4107
|
-
.then(() => bootProfile('channels:start:ready', { ms: (performance.now() - startedAt).toFixed(1) }))
|
|
4108
|
-
.catch((error) => bootProfile('channels:start:failed', {
|
|
4109
|
-
ms: (performance.now() - startedAt).toFixed(1),
|
|
4110
|
-
error: error?.message || String(error),
|
|
4111
|
-
}));
|
|
4259
|
+
void invokeChannelStart();
|
|
4112
4260
|
}, delayMs);
|
|
4113
4261
|
channelStartTimer.unref?.();
|
|
4114
4262
|
}
|
|
4115
4263
|
|
|
4264
|
+
// Remote (Discord channel) mode is opt-in per session. Only a session that
|
|
4265
|
+
// explicitly enables remote — via `mixdog --remote` or the runtime toggle —
|
|
4266
|
+
// boots the channel worker and contends for channel ownership.
|
|
4267
|
+
function startRemote() {
|
|
4268
|
+
remoteEnabled = true;
|
|
4269
|
+
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
4270
|
+
bootProfile('channels:start-skipped');
|
|
4271
|
+
return true;
|
|
4272
|
+
}
|
|
4273
|
+
if (!channelsEnabled()) {
|
|
4274
|
+
bootProfile('channels:start-disabled');
|
|
4275
|
+
return true;
|
|
4276
|
+
}
|
|
4277
|
+
if (closeRequested) return true;
|
|
4278
|
+
if (channelStartTimer) {
|
|
4279
|
+
clearTimeout(channelStartTimer);
|
|
4280
|
+
channelStartTimer = null;
|
|
4281
|
+
}
|
|
4282
|
+
bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
|
|
4283
|
+
void invokeChannelStart();
|
|
4284
|
+
return true;
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
function stopRemote(reason) {
|
|
4288
|
+
remoteEnabled = false;
|
|
4289
|
+
// Cancel any pending deferred start so it can't fire after remote is off.
|
|
4290
|
+
if (channelStartTimer) { clearTimeout(channelStartTimer); channelStartTimer = null; }
|
|
4291
|
+
channels.stop(reason || 'remote-disabled', { waitForExit: false }).catch(() => {});
|
|
4292
|
+
return true;
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
function isRemoteEnabled() {
|
|
4296
|
+
return remoteEnabled;
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4116
4299
|
function withTeardownDeadline(promise, ms, fallback = false) {
|
|
4117
4300
|
let timer = null;
|
|
4118
4301
|
return Promise.race([
|
|
@@ -4140,6 +4323,7 @@ function parsedProviderModelVersion(id) {
|
|
|
4140
4323
|
bootProfile('code-graph:prewarm-lazy', { reason: 'startup-deferred-to-first-turn' });
|
|
4141
4324
|
}
|
|
4142
4325
|
scheduleProviderSetupWarmup();
|
|
4326
|
+
scheduleModelCatalogWarmup();
|
|
4143
4327
|
scheduleProviderWarmup();
|
|
4144
4328
|
// Warm the provider model catalog in the background, but keep it on its own
|
|
4145
4329
|
// delay so short-lived detached runtimes can exit before /models I/O starts.
|
|
@@ -4147,7 +4331,10 @@ function parsedProviderModelVersion(id) {
|
|
|
4147
4331
|
// MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS explicitly.
|
|
4148
4332
|
scheduleProviderModelWarmup();
|
|
4149
4333
|
scheduleStatuslineUsageWarmup();
|
|
4150
|
-
|
|
4334
|
+
// Channels are opt-in: only boot the worker when this session started in (or
|
|
4335
|
+
// was toggled into) remote mode. Non-remote sessions never contend for the
|
|
4336
|
+
// channel; see startRemote()/stopRemote() and the `/remote` toggle.
|
|
4337
|
+
if (remoteEnabled) startRemote();
|
|
4151
4338
|
|
|
4152
4339
|
function contextStatusCacheKeyFor({ messages, tools }) {
|
|
4153
4340
|
const compaction = session?.compaction || {};
|
|
@@ -4174,10 +4361,12 @@ function parsedProviderModelVersion(id) {
|
|
|
4174
4361
|
lastContextTokensUpdatedAt: Number(session?.lastContextTokensUpdatedAt || 0),
|
|
4175
4362
|
lastContextTokensStaleAfterCompact: session?.lastContextTokensStaleAfterCompact === true,
|
|
4176
4363
|
lastInputTokens: Number(session?.lastInputTokens || 0),
|
|
4364
|
+
lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
|
|
4177
4365
|
lastOutputTokens: Number(session?.lastOutputTokens || 0),
|
|
4178
4366
|
lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
|
|
4179
4367
|
lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
|
|
4180
4368
|
totalInputTokens: Number(session?.totalInputTokens || 0),
|
|
4369
|
+
totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
|
|
4181
4370
|
totalOutputTokens: Number(session?.totalOutputTokens || 0),
|
|
4182
4371
|
totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
|
|
4183
4372
|
totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
|
|
@@ -4300,38 +4489,13 @@ function parsedProviderModelVersion(id) {
|
|
|
4300
4489
|
// the gauge numerator.
|
|
4301
4490
|
const usedTokens = estimatedContextTokens;
|
|
4302
4491
|
const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
|
|
4303
|
-
|
|
4304
|
-
//
|
|
4305
|
-
//
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
// An explicit autoCompactTokenLimit (provider/catalog/seed supplied)
|
|
4309
|
-
// still takes precedence and is honored verbatim when it sits at or below
|
|
4310
|
-
// the boundary.
|
|
4311
|
-
const defaultCompactBufferTokens = contextDefaultBufferTokens(compactBoundaryTokens, session?.compaction || {});
|
|
4312
|
-
const defaultCompactTriggerTokens = compactBoundaryTokens
|
|
4313
|
-
? Math.max(1, compactBoundaryTokens - defaultCompactBufferTokens)
|
|
4314
|
-
: 0;
|
|
4315
|
-
// Only an explicit auto-compact limit STRICTLY BELOW the boundary is a
|
|
4316
|
-
// real trigger; a persisted value == boundary is a legacy derived
|
|
4317
|
-
// full-window artifact and must fall through to the default trigger.
|
|
4318
|
-
// Likewise ignore persisted default-buffer telemetry so the displayed
|
|
4319
|
-
// trigger matches what the runtime now actually fires.
|
|
4320
|
-
const persistedTriggerTokens = Number(session?.compaction?.triggerTokens || 0);
|
|
4321
|
-
const legacyDefaultTriggerTelemetry = isLegacyDefaultContextBufferTelemetry(session?.compaction || {}, compactBoundaryTokens)
|
|
4322
|
-
&& persistedTriggerTokens > 0
|
|
4323
|
-
&& persistedTriggerTokens < compactBoundaryTokens;
|
|
4324
|
-
const usablePersistedTrigger = persistedTriggerTokens > 0
|
|
4325
|
-
&& !legacyDefaultTriggerTelemetry
|
|
4326
|
-
&& (!compactBoundaryTokens || persistedTriggerTokens < compactBoundaryTokens)
|
|
4327
|
-
? persistedTriggerTokens
|
|
4328
|
-
: 0;
|
|
4329
|
-
const compactTriggerTokens = autoCompactTokenLimit && compactBoundaryTokens && autoCompactTokenLimit < compactBoundaryTokens
|
|
4330
|
-
? autoCompactTokenLimit
|
|
4331
|
-
: (usablePersistedTrigger || defaultCompactTriggerTokens || 0);
|
|
4332
|
-
const compactBufferTokens = Number(compactBoundaryTokens && compactTriggerTokens
|
|
4492
|
+
// Use the same shared compact-policy math as manager/loop. Do not trust
|
|
4493
|
+
// persisted trigger telemetry as an independent policy input: it is an
|
|
4494
|
+
// output snapshot and was the source of repeated /context false positives.
|
|
4495
|
+
const compactTriggerTokens = resolveCompactTriggerTokens(session || {}, compactBoundaryTokens) || 0;
|
|
4496
|
+
const compactBufferTokens = compactBoundaryTokens
|
|
4333
4497
|
? Math.max(0, compactBoundaryTokens - compactTriggerTokens)
|
|
4334
|
-
:
|
|
4498
|
+
: resolveCompactBufferTokens(compactBoundaryTokens, session?.compaction || {});
|
|
4335
4499
|
const value = {
|
|
4336
4500
|
sessionId: session?.id || null,
|
|
4337
4501
|
provider: route.provider,
|
|
@@ -4366,11 +4530,13 @@ function parsedProviderModelVersion(id) {
|
|
|
4366
4530
|
},
|
|
4367
4531
|
usage: {
|
|
4368
4532
|
lastInputTokens: Number(session?.lastInputTokens || 0),
|
|
4533
|
+
lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
|
|
4369
4534
|
lastOutputTokens: Number(session?.lastOutputTokens || 0),
|
|
4370
4535
|
lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
|
|
4371
4536
|
lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
|
|
4372
4537
|
lastContextTokens,
|
|
4373
4538
|
totalInputTokens: Number(session?.totalInputTokens || 0),
|
|
4539
|
+
totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
|
|
4374
4540
|
totalOutputTokens: Number(session?.totalOutputTokens || 0),
|
|
4375
4541
|
totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
|
|
4376
4542
|
totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
|
|
@@ -4446,6 +4612,21 @@ function parsedProviderModelVersion(id) {
|
|
|
4446
4612
|
workflowRoutes: summarizeWorkflowRoutes(nextConfig),
|
|
4447
4613
|
};
|
|
4448
4614
|
},
|
|
4615
|
+
// Mark onboarding as done WITHOUT touching routes/agents/provider. Used by
|
|
4616
|
+
// the TUI "skip" (Esc) path so the wizard doesn't reappear next launch,
|
|
4617
|
+
// while leaving any existing config routes untouched.
|
|
4618
|
+
skipOnboarding() {
|
|
4619
|
+
const nextConfig = { ...config };
|
|
4620
|
+
nextConfig.onboarding = {
|
|
4621
|
+
...(nextConfig.onboarding || {}),
|
|
4622
|
+
completed: true,
|
|
4623
|
+
version: ONBOARDING_VERSION,
|
|
4624
|
+
completedAt: new Date().toISOString(),
|
|
4625
|
+
skipped: true,
|
|
4626
|
+
};
|
|
4627
|
+
saveConfigAndAdopt(nextConfig);
|
|
4628
|
+
return this.getOnboardingStatus();
|
|
4629
|
+
},
|
|
4449
4630
|
getAutoClear() {
|
|
4450
4631
|
return normalizeAutoClearConfig(config.autoClear);
|
|
4451
4632
|
},
|
|
@@ -4547,7 +4728,9 @@ function parsedProviderModelVersion(id) {
|
|
|
4547
4728
|
}
|
|
4548
4729
|
await channels.stop('settings-disabled', { waitForExit: false }).catch(() => {});
|
|
4549
4730
|
} else {
|
|
4550
|
-
|
|
4731
|
+
// Enabling channels in settings only boots the worker when this session
|
|
4732
|
+
// is in remote mode; otherwise the toggle just persists config.
|
|
4733
|
+
if (remoteEnabled) scheduleChannelStart(0);
|
|
4551
4734
|
}
|
|
4552
4735
|
invalidatePreSessionToolSurface();
|
|
4553
4736
|
return this.getChannelSettings();
|
|
@@ -4582,14 +4765,57 @@ function parsedProviderModelVersion(id) {
|
|
|
4582
4765
|
saveConfigAndAdopt({ ...config, autoClear: next });
|
|
4583
4766
|
return { ...normalizeAutoClearConfig(config.autoClear), label: formatDurationMs(normalizeAutoClearConfig(config.autoClear).idleMs) };
|
|
4584
4767
|
},
|
|
4768
|
+
getUpdateSettings() {
|
|
4769
|
+
return {
|
|
4770
|
+
autoUpdate: autoUpdateEnabled(),
|
|
4771
|
+
currentVersion: updateCheckState.currentVersion || localPackageVersion(),
|
|
4772
|
+
latestVersion: updateCheckState.latestVersion,
|
|
4773
|
+
updateAvailable: updateCheckState.updateAvailable,
|
|
4774
|
+
lastCheckedAt: updateCheckState.lastCheckedAt,
|
|
4775
|
+
};
|
|
4776
|
+
},
|
|
4777
|
+
setAutoUpdate(enabled) {
|
|
4778
|
+
saveConfigAndAdopt({
|
|
4779
|
+
...config,
|
|
4780
|
+
update: { ...(config.update || {}), auto: enabled === true },
|
|
4781
|
+
});
|
|
4782
|
+
return this.getUpdateSettings();
|
|
4783
|
+
},
|
|
4784
|
+
async checkForUpdate(options = {}) {
|
|
4785
|
+
await checkForUpdateInternal({ force: options?.force === true });
|
|
4786
|
+
return this.getUpdateSettings();
|
|
4787
|
+
},
|
|
4788
|
+
async runUpdateNow() {
|
|
4789
|
+
const state = await runUpdateNowInternal();
|
|
4790
|
+
return { ok: state.phase === 'installed', ...state };
|
|
4791
|
+
},
|
|
4792
|
+
getUpdateStatus() {
|
|
4793
|
+
return { ...updateProcessState };
|
|
4794
|
+
},
|
|
4585
4795
|
async completeOnboarding(payload = {}) {
|
|
4586
|
-
|
|
4796
|
+
// Only fall back to the live runtime route when the caller actually sent a
|
|
4797
|
+
// defaultRoute. The onboarding "partial save" path (Main left unset, only
|
|
4798
|
+
// Search/agent picks) omits defaultRoute entirely and must NOT persist the
|
|
4799
|
+
// current route as Main or recreate the session.
|
|
4800
|
+
const defaultRoute = hasOwn(payload, 'defaultRoute')
|
|
4801
|
+
? normalizeWorkflowRoute(payload.defaultRoute, route)
|
|
4802
|
+
: null;
|
|
4587
4803
|
const workflowInput = payload.workflowRoutes && typeof payload.workflowRoutes === 'object'
|
|
4588
4804
|
? payload.workflowRoutes
|
|
4589
4805
|
: {};
|
|
4590
4806
|
const nextConfig = { ...config };
|
|
4807
|
+
if (hasOwn(payload, 'defaultProvider')) {
|
|
4808
|
+
const requested = clean(payload.defaultProvider);
|
|
4809
|
+
if (requested) {
|
|
4810
|
+
if (!isKnownProvider(requested)) throw new Error(`unknown provider "${payload.defaultProvider}"`);
|
|
4811
|
+
nextConfig.defaultProvider = requested;
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4591
4814
|
let presets = Array.isArray(nextConfig.presets) ? nextConfig.presets.slice() : [];
|
|
4592
4815
|
const workflowRoutes = { ...(nextConfig.workflowRoutes || {}) };
|
|
4816
|
+
// Track slots actually written THIS call so maintenance mirroring never
|
|
4817
|
+
// clobbers an existing slot the payload did not touch.
|
|
4818
|
+
const touchedWorkflowSlots = new Set();
|
|
4593
4819
|
|
|
4594
4820
|
if (defaultRoute) {
|
|
4595
4821
|
presets = upsertWorkflowPreset(presets, 'lead', defaultRoute);
|
|
@@ -4602,18 +4828,51 @@ function parsedProviderModelVersion(id) {
|
|
|
4602
4828
|
if (!normalized) continue;
|
|
4603
4829
|
workflowRoutes[slot] = normalized;
|
|
4604
4830
|
presets = upsertWorkflowPreset(presets, slot, normalized);
|
|
4831
|
+
touchedWorkflowSlots.add(slot);
|
|
4605
4832
|
}
|
|
4606
4833
|
|
|
4607
4834
|
nextConfig.presets = presets;
|
|
4608
4835
|
nextConfig.workflowRoutes = workflowRoutes;
|
|
4609
|
-
// Maintenance slots store a direct {provider, model} route.
|
|
4610
|
-
//
|
|
4611
|
-
//
|
|
4836
|
+
// Maintenance slots store a direct {provider, model} route. Only mirror a
|
|
4837
|
+
// slot that was actually written this call (touchedWorkflowSlots); an
|
|
4838
|
+
// untouched slot keeps its existing maintenance value.
|
|
4612
4839
|
nextConfig.maintenance = {
|
|
4613
4840
|
...(nextConfig.maintenance || {}),
|
|
4614
|
-
...(
|
|
4615
|
-
...(
|
|
4841
|
+
...(touchedWorkflowSlots.has('explorer') ? { explore: normalizeWorkflowRoute(workflowRoutes.explorer) } : {}),
|
|
4842
|
+
...(touchedWorkflowSlots.has('memory') ? { memory: normalizeWorkflowRoute(workflowRoutes.memory) } : {}),
|
|
4616
4843
|
};
|
|
4844
|
+
// Per-agent onboarding routes (id → route) for the full FIXED_AGENT_SLOTS
|
|
4845
|
+
// roster. Mirrors setAgentRoute persistence: config.agents[id], an
|
|
4846
|
+
// agent-<id> preset, and (for workflow-backed agents) the workflowRoute +
|
|
4847
|
+
// maintenance slot. Agents omitted from the payload inherit defaultRoute.
|
|
4848
|
+
const agentInput = payload.agentRoutes && typeof payload.agentRoutes === 'object'
|
|
4849
|
+
? payload.agentRoutes
|
|
4850
|
+
: null;
|
|
4851
|
+
if (agentInput) {
|
|
4852
|
+
const nextAgents = { ...(nextConfig.agents || {}) };
|
|
4853
|
+
const nextMaintenance = { ...(nextConfig.maintenance || {}) };
|
|
4854
|
+
for (const agent of FIXED_AGENT_SLOTS) {
|
|
4855
|
+
// Persist ONLY agents with an explicit override in the payload. An
|
|
4856
|
+
// omitted agent is left untouched (existing value preserved, or
|
|
4857
|
+
// unwritten so the runtime dynamically follows the Main Model). Never
|
|
4858
|
+
// fall back to defaultRoute here — that would overwrite an agent the
|
|
4859
|
+
// user did not choose.
|
|
4860
|
+
const routeToSave = normalizeWorkflowRoute(agentInput[agent.id]);
|
|
4861
|
+
if (!routeToSave) continue;
|
|
4862
|
+
nextAgents[agent.id] = routeToSave;
|
|
4863
|
+
presets = upsertWorkflowPreset(presets, agentPresetSlot(agent.id), routeToSave);
|
|
4864
|
+
if (agent.workflowSlot) {
|
|
4865
|
+
workflowRoutes[agent.workflowSlot] = routeToSave;
|
|
4866
|
+
presets = upsertWorkflowPreset(presets, agent.workflowSlot, routeToSave);
|
|
4867
|
+
if (agent.id === 'explore') nextMaintenance.explore = routeToSave;
|
|
4868
|
+
if (agent.id === 'maintainer') nextMaintenance.memory = routeToSave;
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
nextConfig.agents = nextAgents;
|
|
4872
|
+
nextConfig.presets = presets;
|
|
4873
|
+
nextConfig.workflowRoutes = workflowRoutes;
|
|
4874
|
+
nextConfig.maintenance = nextMaintenance;
|
|
4875
|
+
}
|
|
4617
4876
|
nextConfig.onboarding = {
|
|
4618
4877
|
...(nextConfig.onboarding || {}),
|
|
4619
4878
|
completed: true,
|
|
@@ -4621,6 +4880,14 @@ function parsedProviderModelVersion(id) {
|
|
|
4621
4880
|
completedAt: new Date().toISOString(),
|
|
4622
4881
|
};
|
|
4623
4882
|
|
|
4883
|
+
// Optional native web-search route. Only persisted when provided; mirrors
|
|
4884
|
+
// setSearchRoute's config write (skip provider capability validation here
|
|
4885
|
+
// since onboarding routes come from the vetted provider model list).
|
|
4886
|
+
if (payload.searchRoute) {
|
|
4887
|
+
const searchToSave = normalizeSearchRouteConfig(payload.searchRoute);
|
|
4888
|
+
if (searchToSave) nextConfig.searchRoute = searchToSave;
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4624
4891
|
saveConfigAndAdopt(nextConfig);
|
|
4625
4892
|
if (defaultRoute) {
|
|
4626
4893
|
route = resolveRoute(config, { provider: defaultRoute.provider, model: defaultRoute.model, effort: defaultRoute.effort });
|
|
@@ -4635,6 +4902,15 @@ function parsedProviderModelVersion(id) {
|
|
|
4635
4902
|
getChannelWorkerStatus() {
|
|
4636
4903
|
return channels.status();
|
|
4637
4904
|
},
|
|
4905
|
+
startRemote() {
|
|
4906
|
+
return startRemote();
|
|
4907
|
+
},
|
|
4908
|
+
stopRemote(reason) {
|
|
4909
|
+
return stopRemote(reason);
|
|
4910
|
+
},
|
|
4911
|
+
isRemoteEnabled() {
|
|
4912
|
+
return isRemoteEnabled();
|
|
4913
|
+
},
|
|
4638
4914
|
saveDiscordToken(token) {
|
|
4639
4915
|
const result = saveDiscordToken(token);
|
|
4640
4916
|
reloadChannelsSoon();
|
|
@@ -4645,6 +4921,20 @@ function parsedProviderModelVersion(id) {
|
|
|
4645
4921
|
reloadChannelsSoon();
|
|
4646
4922
|
return result;
|
|
4647
4923
|
},
|
|
4924
|
+
saveTelegramToken(token) {
|
|
4925
|
+
const result = saveTelegramToken(token);
|
|
4926
|
+
reloadChannelsSoon();
|
|
4927
|
+
return result;
|
|
4928
|
+
},
|
|
4929
|
+
forgetTelegramToken() {
|
|
4930
|
+
const result = forgetTelegramToken();
|
|
4931
|
+
reloadChannelsSoon();
|
|
4932
|
+
return result;
|
|
4933
|
+
},
|
|
4934
|
+
setBackend(name) {
|
|
4935
|
+
const result = setBackend(name);
|
|
4936
|
+
return result;
|
|
4937
|
+
},
|
|
4648
4938
|
saveWebhookAuthtoken(token) {
|
|
4649
4939
|
const result = saveWebhookAuthtoken(token);
|
|
4650
4940
|
reloadChannelsSoon();
|
|
@@ -4934,6 +5224,13 @@ function parsedProviderModelVersion(id) {
|
|
|
4934
5224
|
saveConfigAndAdopt(nextConfig);
|
|
4935
5225
|
return routeToSave;
|
|
4936
5226
|
},
|
|
5227
|
+
async setDefaultProvider(provider) {
|
|
5228
|
+
const requested = clean(provider);
|
|
5229
|
+
if (!requested) throw new Error('provider is required');
|
|
5230
|
+
if (!isKnownProvider(requested)) throw new Error(`unknown provider "${provider}"`);
|
|
5231
|
+
saveConfigAndAdopt({ ...config, defaultProvider: requested });
|
|
5232
|
+
return requested;
|
|
5233
|
+
},
|
|
4937
5234
|
async ask(prompt, options = {}) {
|
|
4938
5235
|
activeTurnCount += 1;
|
|
4939
5236
|
// Lazy code-graph prewarm: kick off the build ONCE, on the first real
|
|
@@ -4948,6 +5245,32 @@ function parsedProviderModelVersion(id) {
|
|
|
4948
5245
|
try {
|
|
4949
5246
|
await refreshSessionForCwdIfNeeded('cwd-change');
|
|
4950
5247
|
if (!session?.id) await createCurrentSession('turn');
|
|
5248
|
+
// Remote outbound: ensure a transcript writer bound to the current
|
|
5249
|
+
// session.id + cwd. The channel worker's OutputForwarder tails this
|
|
5250
|
+
// JSONL and pushes the surface view to Discord. Gated on remoteEnabled
|
|
5251
|
+
// so non-remote sessions write nothing.
|
|
5252
|
+
if (remoteEnabled) {
|
|
5253
|
+
const twKey = `${session.id}\u0000${currentCwd}`;
|
|
5254
|
+
// Reset per-turn dedup tracker so an identical answer in a later turn
|
|
5255
|
+
// is still written (the guard only prevents same-turn double-write).
|
|
5256
|
+
_lastAppendedAssistant = '';
|
|
5257
|
+
if (_twKey !== twKey) {
|
|
5258
|
+
try {
|
|
5259
|
+
_transcriptWriter = createTranscriptWriter({
|
|
5260
|
+
mixdogHome: mixdogHome(),
|
|
5261
|
+
sessionId: session.id,
|
|
5262
|
+
cwd: currentCwd,
|
|
5263
|
+
pid: process.pid,
|
|
5264
|
+
});
|
|
5265
|
+
_transcriptWriter.writeSessionRecord();
|
|
5266
|
+
_twKey = twKey;
|
|
5267
|
+
} catch (error) {
|
|
5268
|
+
process.stderr.write(`mixdog: transcript-writer: init failed: ${error?.message || error}\n`);
|
|
5269
|
+
_transcriptWriter = null;
|
|
5270
|
+
_twKey = '';
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
4951
5274
|
hooks.emit('turn:start', { sessionId: session.id, prompt, cwd: currentCwd });
|
|
4952
5275
|
// UserPromptSubmit: bridge to the standard hook bus. A hook FAILURE
|
|
4953
5276
|
// must not block the turn, but a genuine blocked===true MUST throw.
|
|
@@ -4976,6 +5299,14 @@ function parsedProviderModelVersion(id) {
|
|
|
4976
5299
|
name: call?.name || 'tool',
|
|
4977
5300
|
callId: call?.id || null,
|
|
4978
5301
|
});
|
|
5302
|
+
// Mirror each tool card the TUI shows into the transcript so the
|
|
5303
|
+
// forwarder renders the same one-line tool summary on Discord.
|
|
5304
|
+
// calls carry { name, arguments, id } (loop.mjs response.toolCalls);
|
|
5305
|
+
// some providers use `input` — accept either.
|
|
5306
|
+
if (remoteEnabled && _transcriptWriter) {
|
|
5307
|
+
try { _transcriptWriter.appendToolUse(call?.name, call?.input ?? call?.arguments); }
|
|
5308
|
+
catch (error) { process.stderr.write(`mixdog: transcript-writer: onToolCall failed: ${error?.message || error}\n`); }
|
|
5309
|
+
}
|
|
4979
5310
|
}
|
|
4980
5311
|
if (typeof options.onToolCall === 'function') {
|
|
4981
5312
|
return await options.onToolCall(iter, calls);
|
|
@@ -4987,9 +5318,40 @@ function parsedProviderModelVersion(id) {
|
|
|
4987
5318
|
{
|
|
4988
5319
|
onTextDelta: options.onTextDelta,
|
|
4989
5320
|
onReasoningDelta: options.onReasoningDelta,
|
|
4990
|
-
onAssistantText:
|
|
5321
|
+
onAssistantText: (text) => {
|
|
5322
|
+
// Capture the same assistant text the TUI renders (final answer +
|
|
5323
|
+
// mid-turn preamble). Feed the writer, then ALWAYS call through to
|
|
5324
|
+
// the caller's hook so terminal rendering is never affected.
|
|
5325
|
+
if (remoteEnabled && _transcriptWriter) {
|
|
5326
|
+
try {
|
|
5327
|
+
const value = typeof text === 'string' ? text : (text == null ? '' : String(text));
|
|
5328
|
+
if (value.trim()) {
|
|
5329
|
+
_transcriptWriter.appendAssistant(value);
|
|
5330
|
+
// Track the last line we wrote so the post-turn final
|
|
5331
|
+
// append below can skip an identical re-write (buffered
|
|
5332
|
+
// providers surface the final answer here AND in result).
|
|
5333
|
+
_lastAppendedAssistant = value;
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
catch (error) { process.stderr.write(`mixdog: transcript-writer: onAssistantText failed: ${error?.message || error}\n`); }
|
|
5337
|
+
}
|
|
5338
|
+
return options.onAssistantText?.(text);
|
|
5339
|
+
},
|
|
4991
5340
|
onUsageDelta: options.onUsageDelta,
|
|
4992
|
-
onToolResult:
|
|
5341
|
+
onToolResult: (message) => {
|
|
5342
|
+
// Surface Edit diffs only: the forwarder reads toolUseResult
|
|
5343
|
+
// {oldString,newString}. Other tool results carry no diff payload,
|
|
5344
|
+
// so we append only when those fields are present.
|
|
5345
|
+
if (remoteEnabled && _transcriptWriter) {
|
|
5346
|
+
try {
|
|
5347
|
+
const tur = message?.toolUseResult;
|
|
5348
|
+
if (tur && (tur.oldString != null || tur.newString != null)) {
|
|
5349
|
+
_transcriptWriter.appendToolResult({ oldString: tur.oldString ?? '', newString: tur.newString ?? '' });
|
|
5350
|
+
}
|
|
5351
|
+
} catch (error) { process.stderr.write(`mixdog: transcript-writer: onToolResult failed: ${error?.message || error}\n`); }
|
|
5352
|
+
}
|
|
5353
|
+
return options.onToolResult?.(message);
|
|
5354
|
+
},
|
|
4993
5355
|
onToolApproval: options.onToolApproval,
|
|
4994
5356
|
onCompactEvent: options.onCompactEvent,
|
|
4995
5357
|
onStageChange: options.onStageChange,
|
|
@@ -5000,6 +5362,24 @@ function parsedProviderModelVersion(id) {
|
|
|
5000
5362
|
},
|
|
5001
5363
|
);
|
|
5002
5364
|
session = mgr.getSession(session.id) || session;
|
|
5365
|
+
// Final assistant text: onAssistantText fires only for buffered
|
|
5366
|
+
// (non-streaming) providers and for mid-turn preamble. Streaming
|
|
5367
|
+
// providers deliver the final answer via onTextDelta (which we do NOT
|
|
5368
|
+
// tap, to avoid per-delta spam), so append the authoritative final
|
|
5369
|
+
// content here from result.content. To avoid double-writing the
|
|
5370
|
+
// buffered case, only append when it differs from the last assistant
|
|
5371
|
+
// line onAssistantText already wrote.
|
|
5372
|
+
if (remoteEnabled && _transcriptWriter) {
|
|
5373
|
+
try {
|
|
5374
|
+
const finalText = result?.content != null ? String(result.content) : '';
|
|
5375
|
+
if (finalText.trim() && finalText !== _lastAppendedAssistant) {
|
|
5376
|
+
_transcriptWriter.appendAssistant(finalText);
|
|
5377
|
+
_lastAppendedAssistant = finalText;
|
|
5378
|
+
}
|
|
5379
|
+
} catch (error) {
|
|
5380
|
+
process.stderr.write(`mixdog: transcript-writer: final append failed: ${error?.message || error}\n`);
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5003
5383
|
hooks.emit('turn:end', { sessionId: session.id, elapsedMs: Date.now() - startedAt });
|
|
5004
5384
|
// Stop: bridge to the standard hook bus. Best-effort; ignore result,
|
|
5005
5385
|
// never throw.
|
|
@@ -5307,7 +5687,15 @@ function parsedProviderModelVersion(id) {
|
|
|
5307
5687
|
return result;
|
|
5308
5688
|
},
|
|
5309
5689
|
async setRoute(next, options = {}) {
|
|
5310
|
-
|
|
5690
|
+
// Model/provider changes take effect on the NEXT session only — never
|
|
5691
|
+
// rewrite a running session's provider/model in place. A live turn's
|
|
5692
|
+
// prompt cache (Anthropic/OpenAI/etc.) is provider-keyed; flipping
|
|
5693
|
+
// session.provider mid-conversation forces a full cache-miss rewrite
|
|
5694
|
+
// of the entire history on the very next turn (seen as a promptΔ
|
|
5695
|
+
// spike + cache_ratio=0% in session-bench). `route` (this closure
|
|
5696
|
+
// variable) still updates immediately so the NEXT createCurrentSession()
|
|
5697
|
+
// picks it up; only the currently-open session is left untouched.
|
|
5698
|
+
const applyToCurrentSession = options?.applyToCurrentSession === true;
|
|
5311
5699
|
const requested = { ...(next || {}) };
|
|
5312
5700
|
validateRequestedModelSelector(config, requested);
|
|
5313
5701
|
const providerExplicitlyRequested = clean(next?.provider) !== '';
|
|
@@ -5335,12 +5723,6 @@ function parsedProviderModelVersion(id) {
|
|
|
5335
5723
|
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
5336
5724
|
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
5337
5725
|
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
5338
|
-
if (!applyToCurrentSession) {
|
|
5339
|
-
persistLeadRoute(selectedRoute);
|
|
5340
|
-
refreshStatuslineUsageSnapshot(route);
|
|
5341
|
-
scheduleStatuslineUsageRefresh();
|
|
5342
|
-
return selectedRoute;
|
|
5343
|
-
}
|
|
5344
5726
|
const leadRoute = persistLeadRoute(selectedRoute);
|
|
5345
5727
|
route = resolveRoute(config, leadRoute
|
|
5346
5728
|
? { model: workflowPresetId('lead') }
|
|
@@ -5348,6 +5730,11 @@ function parsedProviderModelVersion(id) {
|
|
|
5348
5730
|
await refreshRouteEffort(modelMeta);
|
|
5349
5731
|
refreshStatuslineUsageSnapshot(route);
|
|
5350
5732
|
scheduleStatuslineUsageRefresh();
|
|
5733
|
+
if (!applyToCurrentSession) {
|
|
5734
|
+
// `route` is updated for the next session; the open session (and its
|
|
5735
|
+
// live provider/model/cache chain) is left alone on purpose.
|
|
5736
|
+
return route;
|
|
5737
|
+
}
|
|
5351
5738
|
if (session) {
|
|
5352
5739
|
const updated = mgr.updateSessionRoute?.(session.id, {
|
|
5353
5740
|
provider: route.provider,
|
|
@@ -5434,6 +5821,10 @@ function parsedProviderModelVersion(id) {
|
|
|
5434
5821
|
clearTimeout(providerModelWarmupTimer);
|
|
5435
5822
|
providerModelWarmupTimer = null;
|
|
5436
5823
|
}
|
|
5824
|
+
if (modelCatalogWarmupTimer) {
|
|
5825
|
+
clearTimeout(modelCatalogWarmupTimer);
|
|
5826
|
+
modelCatalogWarmupTimer = null;
|
|
5827
|
+
}
|
|
5437
5828
|
if (codeGraphPrewarmTimer) {
|
|
5438
5829
|
clearTimeout(codeGraphPrewarmTimer);
|
|
5439
5830
|
codeGraphPrewarmTimer = null;
|
|
@@ -5512,8 +5903,8 @@ function parsedProviderModelVersion(id) {
|
|
|
5512
5903
|
if (owner && !['cli', 'user', 'mixdog', 'legacy'].includes(owner)) return null;
|
|
5513
5904
|
const sourceType = clean(s.sourceType || '').toLowerCase();
|
|
5514
5905
|
const sourceName = clean(s.sourceName || '').toLowerCase();
|
|
5515
|
-
const
|
|
5516
|
-
const leadish =
|
|
5906
|
+
const agent = clean(s.agent || '').toLowerCase();
|
|
5907
|
+
const leadish = agent === 'lead'
|
|
5517
5908
|
|| sourceType === 'lead'
|
|
5518
5909
|
|| (sourceType === 'cli' && (!sourceName || sourceName === 'main'))
|
|
5519
5910
|
|| (!sourceType && !sourceName && !isAgentOwner(owner));
|