mixdog 0.9.1 → 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 +8 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/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/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/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +710 -318
- 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 -2
- 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 +332 -57
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +59 -32
- 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 +78 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +202 -98
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +183 -20
- 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 +15 -9
- 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/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +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 -1224
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +80 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +184 -19
- 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/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.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/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/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2096 -732
- 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 +83 -7
- 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 +170 -98
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +159 -21
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6965 -2391
- 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 +129 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +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 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +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-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 +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -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
|
}
|
|
@@ -2866,6 +2866,21 @@ export async function createMixdogSessionRuntime({
|
|
|
2866
2866
|
} else {
|
|
2867
2867
|
scheduleCodeGraphPrewarm(changed ? 0 : codeGraphPrewarmDelayMs, changed ? 'cwd-change' : 'cwd');
|
|
2868
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
|
+
}
|
|
2869
2884
|
return currentCwd;
|
|
2870
2885
|
}
|
|
2871
2886
|
|
|
@@ -2879,17 +2894,23 @@ export async function createMixdogSessionRuntime({
|
|
|
2879
2894
|
}
|
|
2880
2895
|
|
|
2881
2896
|
function skillContent(name) {
|
|
2882
|
-
const
|
|
2883
|
-
? contextMod.
|
|
2897
|
+
const res = typeof contextMod.loadSkillResource === 'function'
|
|
2898
|
+
? contextMod.loadSkillResource(name, currentCwd)
|
|
2884
2899
|
: null;
|
|
2885
|
-
if (!
|
|
2886
|
-
return { name, content };
|
|
2900
|
+
if (!res) throw new Error(`skill not found: ${name}`);
|
|
2901
|
+
return { name, content: res.content, dir: res.dir };
|
|
2887
2902
|
}
|
|
2888
2903
|
|
|
2889
2904
|
function skillToolContent(name) {
|
|
2890
2905
|
const skill = skillContent(name);
|
|
2891
|
-
|
|
2892
|
-
|
|
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);
|
|
2893
2914
|
}
|
|
2894
2915
|
|
|
2895
2916
|
function addProjectSkill(input = {}) {
|
|
@@ -2964,19 +2985,52 @@ export async function createMixdogSessionRuntime({
|
|
|
2964
2985
|
};
|
|
2965
2986
|
}
|
|
2966
2987
|
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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'
|
|
2971
2993
|
? config.mcpServers
|
|
2972
2994
|
: {};
|
|
2973
|
-
|
|
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;
|
|
2974
3030
|
try {
|
|
2975
|
-
await
|
|
2976
|
-
}
|
|
2977
|
-
|
|
2978
|
-
? error.failures
|
|
2979
|
-
: [{ name: 'mcp', msg: error?.message || String(error) }];
|
|
3031
|
+
await run;
|
|
3032
|
+
} finally {
|
|
3033
|
+
if (mcpConnectInFlight === run) mcpConnectInFlight = null;
|
|
2980
3034
|
}
|
|
2981
3035
|
return mcpStatus();
|
|
2982
3036
|
}
|
|
@@ -2984,10 +3038,42 @@ export async function createMixdogSessionRuntime({
|
|
|
2984
3038
|
function normalizeMcpServerInput(input = {}) {
|
|
2985
3039
|
const name = clean(input.name).toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
2986
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
|
+
};
|
|
2987
3055
|
const url = clean(input.url);
|
|
3056
|
+
const type = clean(input.type).toLowerCase();
|
|
2988
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
|
+
}
|
|
2989
3075
|
if (!/^https?:\/\//i.test(url)) throw new Error('MCP URL must start with http:// or https://');
|
|
2990
|
-
return { name, config: {
|
|
3076
|
+
return { name, config: withOptionalHeaders({ type: 'http', url }) };
|
|
2991
3077
|
}
|
|
2992
3078
|
const command = clean(input.command);
|
|
2993
3079
|
if (!command) throw new Error('MCP server command or URL is required');
|
|
@@ -3001,7 +3087,10 @@ export async function createMixdogSessionRuntime({
|
|
|
3001
3087
|
if (resolvedCwd !== root && !resolvedCwd.startsWith(`${root}\\`) && !resolvedCwd.startsWith(`${root}/`)) {
|
|
3002
3088
|
throw new Error('MCP server cwd must stay under the current project');
|
|
3003
3089
|
}
|
|
3004
|
-
|
|
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 };
|
|
3005
3094
|
}
|
|
3006
3095
|
|
|
3007
3096
|
const agentToolStartedAt = performance.now();
|
|
@@ -3035,10 +3124,12 @@ export async function createMixdogSessionRuntime({
|
|
|
3035
3124
|
if (msg?.method !== 'notifications/claude/channel') return;
|
|
3036
3125
|
const params = msg?.params && typeof msg.params === 'object' ? msg.params : {};
|
|
3037
3126
|
const meta = params.meta && typeof params.meta === 'object' ? params.meta : {};
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
const
|
|
3041
|
-
|
|
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
|
+
}
|
|
3042
3133
|
},
|
|
3043
3134
|
});
|
|
3044
3135
|
bootProfile('channels:worker-ready', { ms: (performance.now() - channelsStartedAt).toFixed(1) });
|
|
@@ -3169,6 +3260,7 @@ export async function createMixdogSessionRuntime({
|
|
|
3169
3260
|
},
|
|
3170
3261
|
});
|
|
3171
3262
|
internalTools.markBootReady?.();
|
|
3263
|
+
try { lastProjectMcpKey = resolve(currentCwd) + '\u0000' + JSON.stringify(readProjectMcpServers(currentCwd)); } catch { lastProjectMcpKey = null; }
|
|
3172
3264
|
void connectConfiguredMcp()
|
|
3173
3265
|
.then((status) => bootProfile('mcp:ready', {
|
|
3174
3266
|
connected: Number(status?.connectedCount || 0),
|
|
@@ -3183,8 +3275,8 @@ export async function createMixdogSessionRuntime({
|
|
|
3183
3275
|
function invalidateProviderCaches() {
|
|
3184
3276
|
providerModelsCache = { models: null, at: 0 };
|
|
3185
3277
|
providerModelsPromise = null;
|
|
3278
|
+
providerModelsLoadSeq += 1;
|
|
3186
3279
|
searchProviderModelsCache = { models: null, at: 0 };
|
|
3187
|
-
searchProviderModelsPromise = null;
|
|
3188
3280
|
usageDashboardCache = { dashboard: null, at: 0 };
|
|
3189
3281
|
usageDashboardPromise = null;
|
|
3190
3282
|
providerSetupCache = { setup: null, at: 0 };
|
|
@@ -3631,17 +3723,11 @@ function parsedProviderModelVersion(id) {
|
|
|
3631
3723
|
searchProviderModelsCache = { models: rows, at: Date.now() };
|
|
3632
3724
|
return providerModelsFromCacheRows(rows);
|
|
3633
3725
|
}
|
|
3634
|
-
if (
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
return models;
|
|
3639
|
-
})
|
|
3640
|
-
.finally(() => {
|
|
3641
|
-
searchProviderModelsPromise = null;
|
|
3642
|
-
});
|
|
3726
|
+
if (force) {
|
|
3727
|
+
const models = await loadSearchProviderModelsFresh({ forceRefresh: true });
|
|
3728
|
+
searchProviderModelsCache = { models, at: Date.now() };
|
|
3729
|
+
return providerModelsFromCacheRows(models);
|
|
3643
3730
|
}
|
|
3644
|
-
return providerModelsFromCacheRows(await searchProviderModelsPromise);
|
|
3645
3731
|
}
|
|
3646
3732
|
|
|
3647
3733
|
function enabledSearchProviderConfig() {
|
|
@@ -3655,7 +3741,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3655
3741
|
return out;
|
|
3656
3742
|
}
|
|
3657
3743
|
|
|
3658
|
-
async function loadSearchProviderModelsFresh() {
|
|
3744
|
+
async function loadSearchProviderModelsFresh({ forceRefresh = false } = {}) {
|
|
3659
3745
|
const searchProviders = enabledSearchProviderConfig();
|
|
3660
3746
|
const providerNames = Object.keys(searchProviders);
|
|
3661
3747
|
if (!providerNames.length) return [];
|
|
@@ -3664,7 +3750,13 @@ function parsedProviderModelVersion(id) {
|
|
|
3664
3750
|
const provider = reg.getProvider(name);
|
|
3665
3751
|
if (typeof provider?.listModels !== 'function') return [];
|
|
3666
3752
|
try {
|
|
3667
|
-
|
|
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
|
+
}
|
|
3668
3760
|
if (!Array.isArray(models)) return [];
|
|
3669
3761
|
const rows = [];
|
|
3670
3762
|
for (const m of models) {
|
|
@@ -3698,14 +3790,20 @@ function parsedProviderModelVersion(id) {
|
|
|
3698
3790
|
return results;
|
|
3699
3791
|
}
|
|
3700
3792
|
|
|
3701
|
-
async function loadProviderModelsFresh() {
|
|
3793
|
+
async function loadProviderModelsFresh({ forceRefresh = false } = {}) {
|
|
3702
3794
|
ensureFullConfig();
|
|
3703
3795
|
await ensureProvidersReady(config.providers || {});
|
|
3704
3796
|
const allProviders = [...reg.getAllProviders()];
|
|
3705
3797
|
const providerResults = await Promise.all(allProviders.map(async ([name, provider]) => {
|
|
3706
3798
|
if (typeof provider?.listModels !== 'function') return [];
|
|
3707
3799
|
try {
|
|
3708
|
-
|
|
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
|
+
}
|
|
3709
3807
|
if (!Array.isArray(models)) return [];
|
|
3710
3808
|
const rows = [];
|
|
3711
3809
|
for (const m of models) {
|
|
@@ -3740,10 +3838,17 @@ function parsedProviderModelVersion(id) {
|
|
|
3740
3838
|
warmProviderModelCache();
|
|
3741
3839
|
return quickProviderModelRows();
|
|
3742
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
|
+
}
|
|
3743
3847
|
if (!providerModelsPromise) {
|
|
3848
|
+
const seq = ++providerModelsLoadSeq;
|
|
3744
3849
|
providerModelsPromise = loadProviderModelsFresh()
|
|
3745
3850
|
.then((models) => {
|
|
3746
|
-
providerModelsCache = { models, at: Date.now() };
|
|
3851
|
+
if (seq === providerModelsLoadSeq) providerModelsCache = { models, at: Date.now() };
|
|
3747
3852
|
return models;
|
|
3748
3853
|
})
|
|
3749
3854
|
.finally(() => {
|
|
@@ -3755,9 +3860,10 @@ function parsedProviderModelVersion(id) {
|
|
|
3755
3860
|
|
|
3756
3861
|
function warmProviderModelCache() {
|
|
3757
3862
|
if (Array.isArray(providerModelsCache.models) || providerModelsPromise) return providerModelsPromise;
|
|
3863
|
+
const seq = ++providerModelsLoadSeq;
|
|
3758
3864
|
providerModelsPromise = loadProviderModelsFresh()
|
|
3759
3865
|
.then((models) => {
|
|
3760
|
-
providerModelsCache = { models, at: Date.now() };
|
|
3866
|
+
if (seq === providerModelsLoadSeq) providerModelsCache = { models, at: Date.now() };
|
|
3761
3867
|
bootProfile('provider-models:warm-ready', { count: models.length });
|
|
3762
3868
|
return models;
|
|
3763
3869
|
})
|
|
@@ -3794,12 +3900,22 @@ function parsedProviderModelVersion(id) {
|
|
|
3794
3900
|
const requested = hasOwn(route, 'effort') ? route.effort : (route.preset?.effort || null);
|
|
3795
3901
|
const effectiveEffort = coerceEffortFor(route.provider, modelMeta, requested);
|
|
3796
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);
|
|
3797
3912
|
route = {
|
|
3798
3913
|
...route,
|
|
3799
3914
|
fast: fastCapable ? route.fast === true : false,
|
|
3800
3915
|
fastCapable,
|
|
3801
3916
|
effectiveEffort,
|
|
3802
3917
|
effortOptions: effortItemsFor(route.provider, modelMeta, effectiveEffort),
|
|
3918
|
+
...(modelDisplay ? { modelDisplay } : {}),
|
|
3803
3919
|
};
|
|
3804
3920
|
return route;
|
|
3805
3921
|
}
|
|
@@ -3857,7 +3973,7 @@ function parsedProviderModelVersion(id) {
|
|
|
3857
3973
|
preset: route.preset || undefined,
|
|
3858
3974
|
tools: toolSpecForMode(mode),
|
|
3859
3975
|
owner: 'cli',
|
|
3860
|
-
|
|
3976
|
+
agent: 'lead',
|
|
3861
3977
|
lane: 'cli',
|
|
3862
3978
|
sourceType: 'lead',
|
|
3863
3979
|
sourceName: 'main',
|
|
@@ -4021,6 +4137,27 @@ function parsedProviderModelVersion(id) {
|
|
|
4021
4137
|
providerModelWarmupTimer.unref?.();
|
|
4022
4138
|
}
|
|
4023
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
|
+
|
|
4024
4161
|
function scheduleStatuslineUsageWarmup(delayMs = statuslineUsageWarmupDelayMs) {
|
|
4025
4162
|
const providerId = clean(route?.provider);
|
|
4026
4163
|
if (!providerId || !providerId.includes('oauth')) {
|
|
@@ -4081,6 +4218,22 @@ function parsedProviderModelVersion(id) {
|
|
|
4081
4218
|
statuslineUsageRefreshTimer.unref?.();
|
|
4082
4219
|
}
|
|
4083
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
|
+
|
|
4084
4237
|
function scheduleChannelStart(delayMs = channelStartDelayMs) {
|
|
4085
4238
|
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
4086
4239
|
bootProfile('channels:start-skipped');
|
|
@@ -4090,28 +4243,59 @@ function parsedProviderModelVersion(id) {
|
|
|
4090
4243
|
bootProfile('channels:start-disabled');
|
|
4091
4244
|
return;
|
|
4092
4245
|
}
|
|
4093
|
-
if (channelStartTimer || closeRequested) return;
|
|
4246
|
+
if (channelStartTimer || channelStartPromise || closeRequested) return;
|
|
4094
4247
|
bootProfile('channels:start-scheduled', { delayMs });
|
|
4095
4248
|
channelStartTimer = setTimeout(() => {
|
|
4096
4249
|
channelStartTimer = null;
|
|
4097
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;
|
|
4098
4254
|
if (activeTurnCount > 0 || sessionCreatePromise) {
|
|
4099
4255
|
bootProfile('channels:start-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
|
|
4100
4256
|
scheduleChannelStart(backgroundBusyRetryMs);
|
|
4101
4257
|
return;
|
|
4102
4258
|
}
|
|
4103
|
-
|
|
4104
|
-
bootProfile('channels:start:begin');
|
|
4105
|
-
channels.start()
|
|
4106
|
-
.then(() => bootProfile('channels:start:ready', { ms: (performance.now() - startedAt).toFixed(1) }))
|
|
4107
|
-
.catch((error) => bootProfile('channels:start:failed', {
|
|
4108
|
-
ms: (performance.now() - startedAt).toFixed(1),
|
|
4109
|
-
error: error?.message || String(error),
|
|
4110
|
-
}));
|
|
4259
|
+
void invokeChannelStart();
|
|
4111
4260
|
}, delayMs);
|
|
4112
4261
|
channelStartTimer.unref?.();
|
|
4113
4262
|
}
|
|
4114
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
|
+
|
|
4115
4299
|
function withTeardownDeadline(promise, ms, fallback = false) {
|
|
4116
4300
|
let timer = null;
|
|
4117
4301
|
return Promise.race([
|
|
@@ -4139,6 +4323,7 @@ function parsedProviderModelVersion(id) {
|
|
|
4139
4323
|
bootProfile('code-graph:prewarm-lazy', { reason: 'startup-deferred-to-first-turn' });
|
|
4140
4324
|
}
|
|
4141
4325
|
scheduleProviderSetupWarmup();
|
|
4326
|
+
scheduleModelCatalogWarmup();
|
|
4142
4327
|
scheduleProviderWarmup();
|
|
4143
4328
|
// Warm the provider model catalog in the background, but keep it on its own
|
|
4144
4329
|
// delay so short-lived detached runtimes can exit before /models I/O starts.
|
|
@@ -4146,7 +4331,10 @@ function parsedProviderModelVersion(id) {
|
|
|
4146
4331
|
// MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS explicitly.
|
|
4147
4332
|
scheduleProviderModelWarmup();
|
|
4148
4333
|
scheduleStatuslineUsageWarmup();
|
|
4149
|
-
|
|
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();
|
|
4150
4338
|
|
|
4151
4339
|
function contextStatusCacheKeyFor({ messages, tools }) {
|
|
4152
4340
|
const compaction = session?.compaction || {};
|
|
@@ -4173,10 +4361,12 @@ function parsedProviderModelVersion(id) {
|
|
|
4173
4361
|
lastContextTokensUpdatedAt: Number(session?.lastContextTokensUpdatedAt || 0),
|
|
4174
4362
|
lastContextTokensStaleAfterCompact: session?.lastContextTokensStaleAfterCompact === true,
|
|
4175
4363
|
lastInputTokens: Number(session?.lastInputTokens || 0),
|
|
4364
|
+
lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
|
|
4176
4365
|
lastOutputTokens: Number(session?.lastOutputTokens || 0),
|
|
4177
4366
|
lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
|
|
4178
4367
|
lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
|
|
4179
4368
|
totalInputTokens: Number(session?.totalInputTokens || 0),
|
|
4369
|
+
totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
|
|
4180
4370
|
totalOutputTokens: Number(session?.totalOutputTokens || 0),
|
|
4181
4371
|
totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
|
|
4182
4372
|
totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
|
|
@@ -4299,38 +4489,13 @@ function parsedProviderModelVersion(id) {
|
|
|
4299
4489
|
// the gauge numerator.
|
|
4300
4490
|
const usedTokens = estimatedContextTokens;
|
|
4301
4491
|
const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
|
|
4302
|
-
|
|
4303
|
-
//
|
|
4304
|
-
//
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
// An explicit autoCompactTokenLimit (provider/catalog/seed supplied)
|
|
4308
|
-
// still takes precedence and is honored verbatim when it sits at or below
|
|
4309
|
-
// the boundary.
|
|
4310
|
-
const defaultCompactBufferTokens = contextDefaultBufferTokens(compactBoundaryTokens, session?.compaction || {});
|
|
4311
|
-
const defaultCompactTriggerTokens = compactBoundaryTokens
|
|
4312
|
-
? Math.max(1, compactBoundaryTokens - defaultCompactBufferTokens)
|
|
4313
|
-
: 0;
|
|
4314
|
-
// Only an explicit auto-compact limit STRICTLY BELOW the boundary is a
|
|
4315
|
-
// real trigger; a persisted value == boundary is a legacy derived
|
|
4316
|
-
// full-window artifact and must fall through to the default trigger.
|
|
4317
|
-
// Likewise ignore persisted default-buffer telemetry so the displayed
|
|
4318
|
-
// trigger matches what the runtime now actually fires.
|
|
4319
|
-
const persistedTriggerTokens = Number(session?.compaction?.triggerTokens || 0);
|
|
4320
|
-
const legacyDefaultTriggerTelemetry = isLegacyDefaultContextBufferTelemetry(session?.compaction || {}, compactBoundaryTokens)
|
|
4321
|
-
&& persistedTriggerTokens > 0
|
|
4322
|
-
&& persistedTriggerTokens < compactBoundaryTokens;
|
|
4323
|
-
const usablePersistedTrigger = persistedTriggerTokens > 0
|
|
4324
|
-
&& !legacyDefaultTriggerTelemetry
|
|
4325
|
-
&& (!compactBoundaryTokens || persistedTriggerTokens < compactBoundaryTokens)
|
|
4326
|
-
? persistedTriggerTokens
|
|
4327
|
-
: 0;
|
|
4328
|
-
const compactTriggerTokens = autoCompactTokenLimit && compactBoundaryTokens && autoCompactTokenLimit < compactBoundaryTokens
|
|
4329
|
-
? autoCompactTokenLimit
|
|
4330
|
-
: (usablePersistedTrigger || defaultCompactTriggerTokens || 0);
|
|
4331
|
-
const compactBufferTokens = Number(compactBoundaryTokens && compactTriggerTokens
|
|
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
|
|
4332
4497
|
? Math.max(0, compactBoundaryTokens - compactTriggerTokens)
|
|
4333
|
-
:
|
|
4498
|
+
: resolveCompactBufferTokens(compactBoundaryTokens, session?.compaction || {});
|
|
4334
4499
|
const value = {
|
|
4335
4500
|
sessionId: session?.id || null,
|
|
4336
4501
|
provider: route.provider,
|
|
@@ -4365,11 +4530,13 @@ function parsedProviderModelVersion(id) {
|
|
|
4365
4530
|
},
|
|
4366
4531
|
usage: {
|
|
4367
4532
|
lastInputTokens: Number(session?.lastInputTokens || 0),
|
|
4533
|
+
lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
|
|
4368
4534
|
lastOutputTokens: Number(session?.lastOutputTokens || 0),
|
|
4369
4535
|
lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
|
|
4370
4536
|
lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
|
|
4371
4537
|
lastContextTokens,
|
|
4372
4538
|
totalInputTokens: Number(session?.totalInputTokens || 0),
|
|
4539
|
+
totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
|
|
4373
4540
|
totalOutputTokens: Number(session?.totalOutputTokens || 0),
|
|
4374
4541
|
totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
|
|
4375
4542
|
totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
|
|
@@ -4445,6 +4612,21 @@ function parsedProviderModelVersion(id) {
|
|
|
4445
4612
|
workflowRoutes: summarizeWorkflowRoutes(nextConfig),
|
|
4446
4613
|
};
|
|
4447
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
|
+
},
|
|
4448
4630
|
getAutoClear() {
|
|
4449
4631
|
return normalizeAutoClearConfig(config.autoClear);
|
|
4450
4632
|
},
|
|
@@ -4546,7 +4728,9 @@ function parsedProviderModelVersion(id) {
|
|
|
4546
4728
|
}
|
|
4547
4729
|
await channels.stop('settings-disabled', { waitForExit: false }).catch(() => {});
|
|
4548
4730
|
} else {
|
|
4549
|
-
|
|
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);
|
|
4550
4734
|
}
|
|
4551
4735
|
invalidatePreSessionToolSurface();
|
|
4552
4736
|
return this.getChannelSettings();
|
|
@@ -4581,14 +4765,57 @@ function parsedProviderModelVersion(id) {
|
|
|
4581
4765
|
saveConfigAndAdopt({ ...config, autoClear: next });
|
|
4582
4766
|
return { ...normalizeAutoClearConfig(config.autoClear), label: formatDurationMs(normalizeAutoClearConfig(config.autoClear).idleMs) };
|
|
4583
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
|
+
},
|
|
4584
4795
|
async completeOnboarding(payload = {}) {
|
|
4585
|
-
|
|
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;
|
|
4586
4803
|
const workflowInput = payload.workflowRoutes && typeof payload.workflowRoutes === 'object'
|
|
4587
4804
|
? payload.workflowRoutes
|
|
4588
4805
|
: {};
|
|
4589
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
|
+
}
|
|
4590
4814
|
let presets = Array.isArray(nextConfig.presets) ? nextConfig.presets.slice() : [];
|
|
4591
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();
|
|
4592
4819
|
|
|
4593
4820
|
if (defaultRoute) {
|
|
4594
4821
|
presets = upsertWorkflowPreset(presets, 'lead', defaultRoute);
|
|
@@ -4601,18 +4828,51 @@ function parsedProviderModelVersion(id) {
|
|
|
4601
4828
|
if (!normalized) continue;
|
|
4602
4829
|
workflowRoutes[slot] = normalized;
|
|
4603
4830
|
presets = upsertWorkflowPreset(presets, slot, normalized);
|
|
4831
|
+
touchedWorkflowSlots.add(slot);
|
|
4604
4832
|
}
|
|
4605
4833
|
|
|
4606
4834
|
nextConfig.presets = presets;
|
|
4607
4835
|
nextConfig.workflowRoutes = workflowRoutes;
|
|
4608
|
-
// Maintenance slots store a direct {provider, model} route.
|
|
4609
|
-
//
|
|
4610
|
-
//
|
|
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.
|
|
4611
4839
|
nextConfig.maintenance = {
|
|
4612
4840
|
...(nextConfig.maintenance || {}),
|
|
4613
|
-
...(
|
|
4614
|
-
...(
|
|
4841
|
+
...(touchedWorkflowSlots.has('explorer') ? { explore: normalizeWorkflowRoute(workflowRoutes.explorer) } : {}),
|
|
4842
|
+
...(touchedWorkflowSlots.has('memory') ? { memory: normalizeWorkflowRoute(workflowRoutes.memory) } : {}),
|
|
4615
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
|
+
}
|
|
4616
4876
|
nextConfig.onboarding = {
|
|
4617
4877
|
...(nextConfig.onboarding || {}),
|
|
4618
4878
|
completed: true,
|
|
@@ -4620,6 +4880,14 @@ function parsedProviderModelVersion(id) {
|
|
|
4620
4880
|
completedAt: new Date().toISOString(),
|
|
4621
4881
|
};
|
|
4622
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
|
+
|
|
4623
4891
|
saveConfigAndAdopt(nextConfig);
|
|
4624
4892
|
if (defaultRoute) {
|
|
4625
4893
|
route = resolveRoute(config, { provider: defaultRoute.provider, model: defaultRoute.model, effort: defaultRoute.effort });
|
|
@@ -4634,6 +4902,15 @@ function parsedProviderModelVersion(id) {
|
|
|
4634
4902
|
getChannelWorkerStatus() {
|
|
4635
4903
|
return channels.status();
|
|
4636
4904
|
},
|
|
4905
|
+
startRemote() {
|
|
4906
|
+
return startRemote();
|
|
4907
|
+
},
|
|
4908
|
+
stopRemote(reason) {
|
|
4909
|
+
return stopRemote(reason);
|
|
4910
|
+
},
|
|
4911
|
+
isRemoteEnabled() {
|
|
4912
|
+
return isRemoteEnabled();
|
|
4913
|
+
},
|
|
4637
4914
|
saveDiscordToken(token) {
|
|
4638
4915
|
const result = saveDiscordToken(token);
|
|
4639
4916
|
reloadChannelsSoon();
|
|
@@ -4644,6 +4921,20 @@ function parsedProviderModelVersion(id) {
|
|
|
4644
4921
|
reloadChannelsSoon();
|
|
4645
4922
|
return result;
|
|
4646
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
|
+
},
|
|
4647
4938
|
saveWebhookAuthtoken(token) {
|
|
4648
4939
|
const result = saveWebhookAuthtoken(token);
|
|
4649
4940
|
reloadChannelsSoon();
|
|
@@ -4933,6 +5224,13 @@ function parsedProviderModelVersion(id) {
|
|
|
4933
5224
|
saveConfigAndAdopt(nextConfig);
|
|
4934
5225
|
return routeToSave;
|
|
4935
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
|
+
},
|
|
4936
5234
|
async ask(prompt, options = {}) {
|
|
4937
5235
|
activeTurnCount += 1;
|
|
4938
5236
|
// Lazy code-graph prewarm: kick off the build ONCE, on the first real
|
|
@@ -4947,6 +5245,32 @@ function parsedProviderModelVersion(id) {
|
|
|
4947
5245
|
try {
|
|
4948
5246
|
await refreshSessionForCwdIfNeeded('cwd-change');
|
|
4949
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
|
+
}
|
|
4950
5274
|
hooks.emit('turn:start', { sessionId: session.id, prompt, cwd: currentCwd });
|
|
4951
5275
|
// UserPromptSubmit: bridge to the standard hook bus. A hook FAILURE
|
|
4952
5276
|
// must not block the turn, but a genuine blocked===true MUST throw.
|
|
@@ -4975,6 +5299,14 @@ function parsedProviderModelVersion(id) {
|
|
|
4975
5299
|
name: call?.name || 'tool',
|
|
4976
5300
|
callId: call?.id || null,
|
|
4977
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
|
+
}
|
|
4978
5310
|
}
|
|
4979
5311
|
if (typeof options.onToolCall === 'function') {
|
|
4980
5312
|
return await options.onToolCall(iter, calls);
|
|
@@ -4986,9 +5318,40 @@ function parsedProviderModelVersion(id) {
|
|
|
4986
5318
|
{
|
|
4987
5319
|
onTextDelta: options.onTextDelta,
|
|
4988
5320
|
onReasoningDelta: options.onReasoningDelta,
|
|
4989
|
-
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
|
+
},
|
|
4990
5340
|
onUsageDelta: options.onUsageDelta,
|
|
4991
|
-
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
|
+
},
|
|
4992
5355
|
onToolApproval: options.onToolApproval,
|
|
4993
5356
|
onCompactEvent: options.onCompactEvent,
|
|
4994
5357
|
onStageChange: options.onStageChange,
|
|
@@ -4999,6 +5362,24 @@ function parsedProviderModelVersion(id) {
|
|
|
4999
5362
|
},
|
|
5000
5363
|
);
|
|
5001
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
|
+
}
|
|
5002
5383
|
hooks.emit('turn:end', { sessionId: session.id, elapsedMs: Date.now() - startedAt });
|
|
5003
5384
|
// Stop: bridge to the standard hook bus. Best-effort; ignore result,
|
|
5004
5385
|
// never throw.
|
|
@@ -5306,7 +5687,15 @@ function parsedProviderModelVersion(id) {
|
|
|
5306
5687
|
return result;
|
|
5307
5688
|
},
|
|
5308
5689
|
async setRoute(next, options = {}) {
|
|
5309
|
-
|
|
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;
|
|
5310
5699
|
const requested = { ...(next || {}) };
|
|
5311
5700
|
validateRequestedModelSelector(config, requested);
|
|
5312
5701
|
const providerExplicitlyRequested = clean(next?.provider) !== '';
|
|
@@ -5334,12 +5723,6 @@ function parsedProviderModelVersion(id) {
|
|
|
5334
5723
|
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
5335
5724
|
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
5336
5725
|
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
5337
|
-
if (!applyToCurrentSession) {
|
|
5338
|
-
persistLeadRoute(selectedRoute);
|
|
5339
|
-
refreshStatuslineUsageSnapshot(route);
|
|
5340
|
-
scheduleStatuslineUsageRefresh();
|
|
5341
|
-
return selectedRoute;
|
|
5342
|
-
}
|
|
5343
5726
|
const leadRoute = persistLeadRoute(selectedRoute);
|
|
5344
5727
|
route = resolveRoute(config, leadRoute
|
|
5345
5728
|
? { model: workflowPresetId('lead') }
|
|
@@ -5347,6 +5730,11 @@ function parsedProviderModelVersion(id) {
|
|
|
5347
5730
|
await refreshRouteEffort(modelMeta);
|
|
5348
5731
|
refreshStatuslineUsageSnapshot(route);
|
|
5349
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
|
+
}
|
|
5350
5738
|
if (session) {
|
|
5351
5739
|
const updated = mgr.updateSessionRoute?.(session.id, {
|
|
5352
5740
|
provider: route.provider,
|
|
@@ -5433,6 +5821,10 @@ function parsedProviderModelVersion(id) {
|
|
|
5433
5821
|
clearTimeout(providerModelWarmupTimer);
|
|
5434
5822
|
providerModelWarmupTimer = null;
|
|
5435
5823
|
}
|
|
5824
|
+
if (modelCatalogWarmupTimer) {
|
|
5825
|
+
clearTimeout(modelCatalogWarmupTimer);
|
|
5826
|
+
modelCatalogWarmupTimer = null;
|
|
5827
|
+
}
|
|
5436
5828
|
if (codeGraphPrewarmTimer) {
|
|
5437
5829
|
clearTimeout(codeGraphPrewarmTimer);
|
|
5438
5830
|
codeGraphPrewarmTimer = null;
|
|
@@ -5511,8 +5903,8 @@ function parsedProviderModelVersion(id) {
|
|
|
5511
5903
|
if (owner && !['cli', 'user', 'mixdog', 'legacy'].includes(owner)) return null;
|
|
5512
5904
|
const sourceType = clean(s.sourceType || '').toLowerCase();
|
|
5513
5905
|
const sourceName = clean(s.sourceName || '').toLowerCase();
|
|
5514
|
-
const
|
|
5515
|
-
const leadish =
|
|
5906
|
+
const agent = clean(s.agent || '').toLowerCase();
|
|
5907
|
+
const leadish = agent === 'lead'
|
|
5516
5908
|
|| sourceType === 'lead'
|
|
5517
5909
|
|| (sourceType === 'cli' && (!sourceName || sourceName === 'main'))
|
|
5518
5910
|
|| (!sourceType && !sourceName && !isAgentOwner(owner));
|