mixdog 0.9.19 → 0.9.21
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/LICENSE +21 -0
- package/README.md +100 -34
- package/package.json +3 -2
- package/scripts/build-tui.mjs +6 -0
- package/scripts/hook-bus-test.mjs +8 -0
- package/scripts/log-writer-guard-smoke.mjs +131 -0
- package/scripts/reactive-compact-persist-smoke.mjs +22 -6
- package/scripts/recall-bench-cases.json +0 -1
- package/scripts/recall-quality-cases.json +1 -2
- package/scripts/session-ingest-compaction-smoke.mjs +241 -0
- package/scripts/tool-smoke.mjs +150 -45
- package/src/defaults/skills/setup/SKILL.md +327 -0
- package/src/help.mjs +2 -5
- package/src/lib/mixdog-debug.cjs +13 -0
- package/src/mixdog-session-runtime.mjs +7 -3328
- package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
- package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
- package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
- package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
- package/src/runtime/channels/index.mjs +6 -2183
- package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
- package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
- package/src/runtime/channels/lib/network-retry.mjs +23 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +627 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
- package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
- package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
- package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
- package/src/runtime/channels/lib/worker-main.mjs +777 -0
- package/src/runtime/memory/index.mjs +163 -1725
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
- package/src/runtime/memory/lib/http-router.mjs +811 -0
- package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
- package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
- package/src/runtime/memory/lib/memory-embed.mjs +28 -7
- package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
- package/src/runtime/memory/lib/memory.mjs +39 -0
- package/src/runtime/memory/lib/query-handlers.mjs +2 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
- package/src/runtime/shared/atomic-file.mjs +138 -80
- package/src/runtime/shared/child-guardian.mjs +61 -3
- package/src/session-runtime/boot-profile.mjs +36 -0
- package/src/session-runtime/channel-config-api.mjs +70 -0
- package/src/session-runtime/context-status.mjs +181 -0
- package/src/session-runtime/env.mjs +17 -0
- package/src/session-runtime/lifecycle-api.mjs +242 -0
- package/src/session-runtime/model-route-api.mjs +198 -0
- package/src/session-runtime/provider-auth-api.mjs +135 -0
- package/src/session-runtime/resource-api.mjs +282 -0
- package/src/session-runtime/runtime-core.mjs +2104 -0
- package/src/session-runtime/session-turn-api.mjs +274 -0
- package/src/session-runtime/tool-catalog.mjs +18 -264
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/session-runtime/workflow-agents-api.mjs +238 -0
- package/src/standalone/agent-tool.mjs +2 -2
- package/src/standalone/channel-worker.mjs +67 -7
- package/src/standalone/folder-dialog.mjs +4 -1
- package/src/standalone/memory-runtime-proxy.mjs +154 -17
- package/src/standalone/seeds.mjs +28 -1
- package/src/tui/App.jsx +40 -28
- package/src/tui/app/core-memory-picker.mjs +1 -1
- package/src/tui/app/doctor.mjs +175 -0
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +9 -0
- package/src/tui/app/use-mouse-input.mjs +6 -0
- package/src/tui/app/use-transcript-scroll.mjs +77 -3
- package/src/tui/dist/index.mjs +2851 -2162
- package/src/tui/engine/context-state.mjs +145 -0
- package/src/tui/engine/prompt-history.mjs +27 -0
- package/src/tui/engine/session-api-ext.mjs +478 -0
- package/src/tui/engine/session-api.mjs +564 -0
- package/src/tui/engine/session-flow.mjs +485 -0
- package/src/tui/engine/turn.mjs +1078 -0
- package/src/tui/engine.mjs +68 -2620
- package/src/tui/index.jsx +7 -0
- package/vendor/ink/build/ink.js +16 -1
- package/vendor/ink/build/output.js +30 -4
- package/vendor/ink/build/render.js +5 -0
- package/src/workflows/sequential/WORKFLOW.md +0 -51
|
@@ -1,3328 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
import { createStandaloneAgent } from './standalone/agent-tool.mjs';
|
|
9
|
-
import { isAgentOwner } from './runtime/agent/orchestrator/agent-owner.mjs';
|
|
10
|
-
import { EXPLORE_TOOL, runExplore } from './standalone/explore-tool.mjs';
|
|
11
|
-
import { createStandaloneChannelWorker } from './standalone/channel-worker.mjs';
|
|
12
|
-
import { createStandaloneMemoryRuntime } from './standalone/memory-runtime-proxy.mjs';
|
|
13
|
-
import { createStandaloneHookBus } from './standalone/hook-bus.mjs';
|
|
14
|
-
import { writeLastSessionCwd } from './runtime/shared/user-cwd.mjs';
|
|
15
|
-
import { cancelBackgroundTasks } from './runtime/shared/background-tasks.mjs';
|
|
16
|
-
import { createTranscriptWriter } from './runtime/shared/transcript-writer.mjs';
|
|
17
|
-
import { mixdogHome } from './runtime/shared/plugin-paths.mjs';
|
|
18
|
-
import { checkLatestVersion, runGlobalUpdate, localPackageVersion } from './runtime/shared/update-checker.mjs';
|
|
19
|
-
import {
|
|
20
|
-
modelVisibleToolCompletionMessage,
|
|
21
|
-
shouldPersistModelVisibleToolCompletion,
|
|
22
|
-
} from './runtime/shared/tool-execution-contract.mjs';
|
|
23
|
-
import {
|
|
24
|
-
channelNotificationModelContent,
|
|
25
|
-
shouldMirrorChannelNotificationToPending,
|
|
26
|
-
} from './runtime/shared/channel-notification-routing.mjs';
|
|
27
|
-
import {
|
|
28
|
-
normalizeAgentPermissionOrNone,
|
|
29
|
-
readMarkdownDocument,
|
|
30
|
-
} from './runtime/shared/markdown-frontmatter.mjs';
|
|
31
|
-
import { setConfiguredShell } from './runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs';
|
|
32
|
-
import { hasUserConversationMessage } from './runtime/agent/orchestrator/session/manager/prompt-utils.mjs';
|
|
33
|
-
import {
|
|
34
|
-
beginOAuthProviderLogin,
|
|
35
|
-
forgetProviderAuth,
|
|
36
|
-
isKnownProvider,
|
|
37
|
-
loginOAuthProvider,
|
|
38
|
-
providerSetup,
|
|
39
|
-
renderProviderStatus,
|
|
40
|
-
saveOpenAIUsageSessionKey,
|
|
41
|
-
saveOpenCodeGoUsageAuth,
|
|
42
|
-
loginOpenCodeGoUsage,
|
|
43
|
-
saveProviderApiKey,
|
|
44
|
-
setLocalProvider,
|
|
45
|
-
} from './standalone/provider-admin.mjs';
|
|
46
|
-
import { createUsageDashboard } from './standalone/usage-dashboard.mjs';
|
|
47
|
-
import { fetchOAuthUsageSnapshot } from './runtime/agent/orchestrator/providers/oauth-usage.mjs';
|
|
48
|
-
import {
|
|
49
|
-
getModelMetadataSync,
|
|
50
|
-
warmCatalogsInBackground,
|
|
51
|
-
} from './runtime/agent/orchestrator/providers/model-catalog.mjs';
|
|
52
|
-
import {
|
|
53
|
-
isResponsesFreeformTool,
|
|
54
|
-
toResponsesCustomTool,
|
|
55
|
-
} from './runtime/agent/orchestrator/providers/custom-tool-wire.mjs';
|
|
56
|
-
import {
|
|
57
|
-
channelSetup,
|
|
58
|
-
deleteSchedule,
|
|
59
|
-
deleteWebhook,
|
|
60
|
-
forgetDiscordToken,
|
|
61
|
-
forgetTelegramToken,
|
|
62
|
-
forgetWebhookAuthtoken,
|
|
63
|
-
setChannel,
|
|
64
|
-
saveDiscordToken,
|
|
65
|
-
saveTelegramToken,
|
|
66
|
-
saveSchedule,
|
|
67
|
-
saveWebhook,
|
|
68
|
-
saveWebhookAuthtoken,
|
|
69
|
-
setBackend,
|
|
70
|
-
setScheduleEnabled,
|
|
71
|
-
setWebhookEnabled,
|
|
72
|
-
setWebhookConfig,
|
|
73
|
-
} from './standalone/channel-admin.mjs';
|
|
74
|
-
import {
|
|
75
|
-
addPlugin as registryAddPlugin,
|
|
76
|
-
listRegisteredPlugins,
|
|
77
|
-
pluginAdminStatus,
|
|
78
|
-
removePlugin as registryRemovePlugin,
|
|
79
|
-
updatePlugin as registryUpdatePlugin,
|
|
80
|
-
} from './standalone/plugin-admin.mjs';
|
|
81
|
-
import {
|
|
82
|
-
estimateMessagesTokens,
|
|
83
|
-
estimateRequestReserveTokens,
|
|
84
|
-
estimateTranscriptContextUsage,
|
|
85
|
-
estimateToolSchemaTokens,
|
|
86
|
-
resolveCompactBufferTokens,
|
|
87
|
-
resolveCompactTriggerTokens,
|
|
88
|
-
summarizeContextMessages,
|
|
89
|
-
} from './runtime/agent/orchestrator/session/context-utils.mjs';
|
|
90
|
-
|
|
91
|
-
import {
|
|
92
|
-
sessionMessageText,
|
|
93
|
-
messageContextText,
|
|
94
|
-
isSessionPreviewNoise,
|
|
95
|
-
cleanSessionPreview,
|
|
96
|
-
clean,
|
|
97
|
-
hasOwn,
|
|
98
|
-
toolResponseText,
|
|
99
|
-
isEmptyRecallText,
|
|
100
|
-
currentSessionRecallRows,
|
|
101
|
-
sessionHasConversationMessages,
|
|
102
|
-
} from './session-runtime/session-text.mjs';
|
|
103
|
-
import {
|
|
104
|
-
TOOL_MODES,
|
|
105
|
-
ALL_EFFORT_LEVELS,
|
|
106
|
-
EFFORT_LABELS,
|
|
107
|
-
EFFORT_OPTIONS_BY_PROVIDER,
|
|
108
|
-
EFFORT_BY_FAMILY,
|
|
109
|
-
EFFORT_FALLBACKS,
|
|
110
|
-
normalizeToolMode,
|
|
111
|
-
normalizeEffortInput,
|
|
112
|
-
effortOptionsFor,
|
|
113
|
-
coerceEffortFor,
|
|
114
|
-
normalizeSavedEffort,
|
|
115
|
-
effortItemsFor,
|
|
116
|
-
toolSpecForMode,
|
|
117
|
-
deferredSurfaceModeForLead,
|
|
118
|
-
} from './session-runtime/effort.mjs';
|
|
119
|
-
import {
|
|
120
|
-
LAZY_SECRET_PROVIDERS,
|
|
121
|
-
routeFastKey,
|
|
122
|
-
fastCapableFor,
|
|
123
|
-
makeSearchCapableFor,
|
|
124
|
-
fastPreferenceFor,
|
|
125
|
-
saveModelSettings,
|
|
126
|
-
} from './session-runtime/model-capabilities.mjs';
|
|
127
|
-
import {
|
|
128
|
-
DEFAULT_PROVIDER,
|
|
129
|
-
DEFAULT_MODEL,
|
|
130
|
-
makeResolveDefaultProvider,
|
|
131
|
-
findPreset,
|
|
132
|
-
makeResolveRoute,
|
|
133
|
-
isLikelyRawModelId,
|
|
134
|
-
validateRequestedModelSelector,
|
|
135
|
-
ensureProviderEnabled,
|
|
136
|
-
normalizeSystemShellConfig,
|
|
137
|
-
normalizeSystemShellCommand,
|
|
138
|
-
normalizeAutoClearConfig,
|
|
139
|
-
resolveAutoClearIdleMs,
|
|
140
|
-
autoClearIdleMsForProvider,
|
|
141
|
-
autoClearProviderDefaults,
|
|
142
|
-
normalizeCompactionConfig,
|
|
143
|
-
moduleEnabled,
|
|
144
|
-
setModuleEnabledInConfig,
|
|
145
|
-
recapEnabled,
|
|
146
|
-
setRecapEnabledInConfig,
|
|
147
|
-
formatDurationMs,
|
|
148
|
-
parseDurationMs,
|
|
149
|
-
modelMetaLooksResolved,
|
|
150
|
-
modelSettingsFor,
|
|
151
|
-
normalizeCompactTypeSetting,
|
|
152
|
-
} from './session-runtime/config-helpers.mjs';
|
|
153
|
-
import {
|
|
154
|
-
routeForStatusline,
|
|
155
|
-
writeStatuslineRoute,
|
|
156
|
-
} from './session-runtime/statusline-route.mjs';
|
|
157
|
-
import {
|
|
158
|
-
normalizeOutputStyleId,
|
|
159
|
-
listOutputStyleCatalog,
|
|
160
|
-
findOutputStyle,
|
|
161
|
-
outputStyleStatus as outputStyleStatusRaw,
|
|
162
|
-
} from './session-runtime/output-styles.mjs';
|
|
163
|
-
import { readJsonSafe, readTextSafe } from './session-runtime/fs-utils.mjs';
|
|
164
|
-
import {
|
|
165
|
-
readProjectMcpServers,
|
|
166
|
-
countSkillFiles,
|
|
167
|
-
mcpScriptForPlugin,
|
|
168
|
-
normalizePluginMcpServerConfig,
|
|
169
|
-
pluginManifest,
|
|
170
|
-
pluginMcpServerName,
|
|
171
|
-
pluginRawMcpServers,
|
|
172
|
-
pluginMcpEnableScript,
|
|
173
|
-
resolveContainedPluginPath,
|
|
174
|
-
} from './session-runtime/plugin-mcp.mjs';
|
|
175
|
-
import {
|
|
176
|
-
WORKFLOW_ROUTE_SLOTS,
|
|
177
|
-
FIXED_AGENT_SLOTS,
|
|
178
|
-
SEARCH_DEFAULT_PROVIDER,
|
|
179
|
-
SEARCH_DEFAULT_MODEL,
|
|
180
|
-
workflowPresetId,
|
|
181
|
-
agentPresetSlot,
|
|
182
|
-
normalizeAgentId,
|
|
183
|
-
normalizeWorkflowId,
|
|
184
|
-
DEFAULT_WORKFLOW_ID,
|
|
185
|
-
createWorkflowHelpers,
|
|
186
|
-
normalizeSearchProviderId,
|
|
187
|
-
isDefaultSearchRouteConfig,
|
|
188
|
-
isSearchCapableProvider,
|
|
189
|
-
normalizeSearchRouteConfig,
|
|
190
|
-
normalizeWorkflowRoute,
|
|
191
|
-
upsertWorkflowPreset,
|
|
192
|
-
createWorkflowRouteHelpers,
|
|
193
|
-
} from './session-runtime/workflow.mjs';
|
|
194
|
-
import {
|
|
195
|
-
MEASURED_TOOL_USAGE,
|
|
196
|
-
DEFERRED_DEFAULT_FULL_TOOLS,
|
|
197
|
-
DEFERRED_DEFAULT_READONLY_TOOLS,
|
|
198
|
-
DEFERRED_DEFAULT_LEAD_TOOLS,
|
|
199
|
-
toolKind,
|
|
200
|
-
toolSchemaBucket,
|
|
201
|
-
estimateToolSchemaBreakdown,
|
|
202
|
-
measuredToolUsage,
|
|
203
|
-
parseToolSelection,
|
|
204
|
-
parseToolSearchQuerySelection,
|
|
205
|
-
sortedCatalogByMeasuredUsage,
|
|
206
|
-
filterDisallowedTools,
|
|
207
|
-
sortedNamesByMeasuredUsage,
|
|
208
|
-
defaultDeferredToolNames,
|
|
209
|
-
compactToolSearchDescription,
|
|
210
|
-
toolRow,
|
|
211
|
-
toolSearchMatches,
|
|
212
|
-
applyDeferredToolSurface,
|
|
213
|
-
selectDeferredTools,
|
|
214
|
-
renderToolSearch,
|
|
215
|
-
} from './session-runtime/tool-catalog.mjs';
|
|
216
|
-
// Re-exported for external consumers (scripts/tool-smoke.mjs) that imported
|
|
217
|
-
// these from this module before the tool-catalog extraction.
|
|
218
|
-
export { defaultDeferredToolNames, compactToolSearchDescription } from './session-runtime/tool-catalog.mjs';
|
|
219
|
-
import {
|
|
220
|
-
TOOL_SEARCH_TOOL,
|
|
221
|
-
CWD_TOOL,
|
|
222
|
-
SKILL_TOOL,
|
|
223
|
-
SESSION_MANAGE_TOOL,
|
|
224
|
-
LEAD_DISALLOWED_TOOLS,
|
|
225
|
-
applyStandaloneToolDefaults,
|
|
226
|
-
} from './session-runtime/tool-defs.mjs';
|
|
227
|
-
import { ONBOARDING_VERSION, QUICK_SEARCH_MODELS } from './session-runtime/quick-search-models.mjs';
|
|
228
|
-
import {
|
|
229
|
-
sortProviderModels as sortProviderModelsRaw,
|
|
230
|
-
providerModelCacheRow as providerModelCacheRowRaw,
|
|
231
|
-
} from './session-runtime/model-recency.mjs';
|
|
232
|
-
import { createNativeSearch } from './session-runtime/native-search.mjs';
|
|
233
|
-
import { createConfigLifecycle } from './session-runtime/config-lifecycle.mjs';
|
|
234
|
-
import { attachSessionHooks } from './session-runtime/session-hooks.mjs';
|
|
235
|
-
import { createQuickModelRows } from './session-runtime/quick-model-rows.mjs';
|
|
236
|
-
import { createWarmupSchedulers } from './session-runtime/warmup-schedulers.mjs';
|
|
237
|
-
import { createPrewarmSchedulers } from './session-runtime/prewarm.mjs';
|
|
238
|
-
import { createMcpGlue } from './session-runtime/mcp-glue.mjs';
|
|
239
|
-
import { createCwdPlugins } from './session-runtime/cwd-plugins.mjs';
|
|
240
|
-
import { createSettingsApi } from './session-runtime/settings-api.mjs';
|
|
241
|
-
import { createProviderModels } from './session-runtime/provider-models.mjs';
|
|
242
|
-
import { createProviderUsage } from './session-runtime/provider-usage.mjs';
|
|
243
|
-
// Re-exported for external consumers (scripts/tool-smoke.mjs) that imported
|
|
244
|
-
// these from this module before the tool-defs extraction.
|
|
245
|
-
export { TOOL_SEARCH_TOOL, SKILL_TOOL };
|
|
246
|
-
// Back-compat test alias; delegates to the extracted helper.
|
|
247
|
-
export function __applyStandaloneToolDefaultsForTest(tool) {
|
|
248
|
-
return applyStandaloneToolDefaults(tool);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const RUNTIME = './runtime/agent/orchestrator';
|
|
252
|
-
const SEARCH_RUNTIME = './runtime/search/index.mjs';
|
|
253
|
-
const SEARCH_TOOL_DEFS = './runtime/search/tool-defs.mjs';
|
|
254
|
-
const MEMORY_TOOL_DEFS = './runtime/memory/tool-defs.mjs';
|
|
255
|
-
const MEMORY_RUNTIME = './runtime/memory/index.mjs';
|
|
256
|
-
const CHANNEL_TOOL_DEFS = './runtime/channels/tool-defs.mjs';
|
|
257
|
-
const CHANNEL_WORKER_ENTRY = './runtime/channels/index.mjs';
|
|
258
|
-
const CODE_GRAPH_TOOL_DEFS = './runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs';
|
|
259
|
-
const CODE_GRAPH_RUNTIME = './runtime/agent/orchestrator/tools/code-graph.mjs';
|
|
260
|
-
const STATUSLINE_SESSION_ROUTES = './vendor/statusline/src/gateway/session-routes.mjs';
|
|
261
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
262
|
-
const STANDALONE_SOURCE_ROOT = __dirname;
|
|
263
|
-
// Resource root stays at src/ because defaults/, rules/, runtime/, vendor/ live
|
|
264
|
-
// there. User-owned standalone state lives under MIXDOG_HOME (~/.mixdog).
|
|
265
|
-
const STANDALONE_ROOT = STANDALONE_SOURCE_ROOT;
|
|
266
|
-
const MIXDOG_HOME = process.env.MIXDOG_HOME || join(homedir(), '.mixdog');
|
|
267
|
-
const STANDALONE_DATA_DIR = process.env.MIXDOG_DATA_DIR || join(MIXDOG_HOME, 'data');
|
|
268
|
-
|
|
269
|
-
const resolveDefaultProvider = makeResolveDefaultProvider(isKnownProvider);
|
|
270
|
-
const resolveRoute = makeResolveRoute(resolveDefaultProvider);
|
|
271
|
-
const searchCapableFor = makeSearchCapableFor(normalizeSearchProviderId, isSearchCapableProvider);
|
|
272
|
-
|
|
273
|
-
function envFlag(name) {
|
|
274
|
-
return /^(1|true|yes|on)$/i.test(String(process.env[name] || ''));
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function envPresent(name) {
|
|
278
|
-
return process.env[name] !== undefined && process.env[name] !== '';
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function envDelayMs(name, fallback, { min = 0, max = 60_000 } = {}) {
|
|
282
|
-
const raw = process.env[name];
|
|
283
|
-
if (raw === undefined || raw === '') return fallback;
|
|
284
|
-
const n = Number(raw);
|
|
285
|
-
if (!Number.isFinite(n)) return fallback;
|
|
286
|
-
return Math.min(max, Math.max(min, Math.floor(n)));
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const BOOT_PROFILE_ENABLED = envFlag('MIXDOG_BOOT_PROFILE');
|
|
290
|
-
const BOOT_PROFILE_START = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance.now());
|
|
291
|
-
|
|
292
|
-
function bootProfile(event, fields = {}) {
|
|
293
|
-
if (!BOOT_PROFILE_ENABLED) return;
|
|
294
|
-
const elapsedMs = performance.now() - BOOT_PROFILE_START;
|
|
295
|
-
const parts = [`[mixdog-boot] +${elapsedMs.toFixed(1)}ms`, event];
|
|
296
|
-
for (const [key, value] of Object.entries(fields || {})) {
|
|
297
|
-
if (value === undefined || value === null || value === '') continue;
|
|
298
|
-
parts.push(`${key}=${String(value).replace(/\s+/g, '_')}`);
|
|
299
|
-
}
|
|
300
|
-
try { process.stderr.write(`${parts.join(' ')}\n`); } catch {}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async function profiledImport(label, spec, { optional = false } = {}) {
|
|
304
|
-
const startedAt = performance.now();
|
|
305
|
-
try {
|
|
306
|
-
const mod = await import(spec);
|
|
307
|
-
bootProfile(`import:${label}`, { ms: (performance.now() - startedAt).toFixed(1) });
|
|
308
|
-
return mod;
|
|
309
|
-
} catch (error) {
|
|
310
|
-
bootProfile(`import:${label}:failed`, {
|
|
311
|
-
ms: (performance.now() - startedAt).toFixed(1),
|
|
312
|
-
error: error?.message || String(error),
|
|
313
|
-
});
|
|
314
|
-
if (optional) return null;
|
|
315
|
-
throw error;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
const outputStyleStatus = (dataDir = STANDALONE_DATA_DIR, opts = {}) => outputStyleStatusRaw(STANDALONE_ROOT, dataDir || STANDALONE_DATA_DIR, opts);
|
|
319
|
-
// Workflow/agent pack loaders bound to this runtime's root/data layout.
|
|
320
|
-
const {
|
|
321
|
-
listWorkflowPacks,
|
|
322
|
-
activeWorkflowId,
|
|
323
|
-
loadWorkflowPack,
|
|
324
|
-
workflowSummary,
|
|
325
|
-
activeWorkflowSummary,
|
|
326
|
-
loadAgentDefinition,
|
|
327
|
-
workflowContextBlock,
|
|
328
|
-
activeWorkflowContext,
|
|
329
|
-
} = createWorkflowHelpers({
|
|
330
|
-
rootDir: STANDALONE_ROOT,
|
|
331
|
-
dataDir: STANDALONE_DATA_DIR,
|
|
332
|
-
readMarkdownDocument,
|
|
333
|
-
normalizeAgentPermissionOrNone,
|
|
334
|
-
});
|
|
335
|
-
const {
|
|
336
|
-
summarizeWorkflowRoutes,
|
|
337
|
-
routeFromPreset,
|
|
338
|
-
agentRouteFromConfig,
|
|
339
|
-
} = createWorkflowRouteHelpers({ resolveDefaultProvider, findPreset });
|
|
340
|
-
|
|
341
|
-
export function __renderToolSearchForTest(args = {}, session = {}, mode = 'full') {
|
|
342
|
-
return renderToolSearch(args, session, mode);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
export function __saveModelSettingsForTest(cfgMod, route, options = {}) {
|
|
346
|
-
return saveModelSettings(cfgMod, route, options);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export async function createMixdogSessionRuntime({
|
|
350
|
-
provider,
|
|
351
|
-
model,
|
|
352
|
-
cwd = process.cwd(),
|
|
353
|
-
toolMode = 'full',
|
|
354
|
-
remote = false,
|
|
355
|
-
} = {}) {
|
|
356
|
-
bootProfile('session-runtime:start', { provider, model, toolMode, cwd });
|
|
357
|
-
let remoteEnabled = remote === true;
|
|
358
|
-
// Remote-mode transcript writer (Discord outbound). Lazily created per
|
|
359
|
-
// session.id + cwd inside ask(); only active while remoteEnabled.
|
|
360
|
-
let _transcriptWriter = null;
|
|
361
|
-
let _twKey = '';
|
|
362
|
-
// Last assistant text handed to the transcript writer (via onAssistantText),
|
|
363
|
-
// so the post-turn final-content append can skip an exact duplicate.
|
|
364
|
-
let _lastAppendedAssistant = '';
|
|
365
|
-
process.env.MIXDOG_QUIET_SESSION_LOG ??= '1';
|
|
366
|
-
const standaloneStartedAt = performance.now();
|
|
367
|
-
ensureStandaloneEnvironment({
|
|
368
|
-
rootDir: STANDALONE_ROOT,
|
|
369
|
-
dataDir: STANDALONE_DATA_DIR,
|
|
370
|
-
});
|
|
371
|
-
bootProfile('standalone-env:ready', { ms: (performance.now() - standaloneStartedAt).toFixed(1) });
|
|
372
|
-
|
|
373
|
-
const importsStartedAt = performance.now();
|
|
374
|
-
const [
|
|
375
|
-
cfgMod,
|
|
376
|
-
sharedCfgMod,
|
|
377
|
-
reg,
|
|
378
|
-
mcpClient,
|
|
379
|
-
mgr,
|
|
380
|
-
contextMod,
|
|
381
|
-
internalTools,
|
|
382
|
-
statusRoutes,
|
|
383
|
-
searchToolDefs,
|
|
384
|
-
memoryToolDefs,
|
|
385
|
-
channelToolDefs,
|
|
386
|
-
codeGraphToolDefs,
|
|
387
|
-
] = await Promise.all([
|
|
388
|
-
profiledImport('config', `${RUNTIME}/config.mjs`),
|
|
389
|
-
profiledImport('shared-config', `${RUNTIME}/../../shared/config.mjs`),
|
|
390
|
-
profiledImport('providers-registry', `${RUNTIME}/providers/registry.mjs`),
|
|
391
|
-
profiledImport('mcp-client', `${RUNTIME}/mcp/client.mjs`),
|
|
392
|
-
profiledImport('session-manager', `${RUNTIME}/session/manager.mjs`),
|
|
393
|
-
profiledImport('context-collect', `${RUNTIME}/context/collect.mjs`),
|
|
394
|
-
profiledImport('internal-tools', `${RUNTIME}/internal-tools.mjs`),
|
|
395
|
-
profiledImport('status-routes', STATUSLINE_SESSION_ROUTES, { optional: true }),
|
|
396
|
-
profiledImport('search-tool-defs', SEARCH_TOOL_DEFS, { optional: true }),
|
|
397
|
-
profiledImport('memory-tool-defs', MEMORY_TOOL_DEFS, { optional: true }),
|
|
398
|
-
profiledImport('channel-tool-defs', CHANNEL_TOOL_DEFS, { optional: true }),
|
|
399
|
-
profiledImport('code-graph-tool-defs', CODE_GRAPH_TOOL_DEFS, { optional: true }),
|
|
400
|
-
]);
|
|
401
|
-
bootProfile('imports:ready', { ms: (performance.now() - importsStartedAt).toFixed(1) });
|
|
402
|
-
const pluginDataDir = cfgMod.getPluginData();
|
|
403
|
-
// Re-wire the idle/tombstone sweep. startIdleCleanup() lost its caller in a
|
|
404
|
-
// refactor, so closed-session tombstones were never deleted after their 24h
|
|
405
|
-
// grace — the store grew unbounded (observed: 1.8k files / 114MB), which
|
|
406
|
-
// made summary-index rebuilds and per-save index rewrites stall boot for
|
|
407
|
-
// seconds. Timer is unref'd and first fires after CLEANUP_INITIAL_DELAY_MS
|
|
408
|
-
// (5min), so this adds zero boot-path cost.
|
|
409
|
-
try { mgr.startIdleCleanup?.(); } catch { /* cleanup is best-effort */ }
|
|
410
|
-
const memoryRuntime = createStandaloneMemoryRuntime({
|
|
411
|
-
entry: join(STANDALONE_ROOT, MEMORY_RUNTIME.replace(/^\.\//, '')),
|
|
412
|
-
dataDir: pluginDataDir,
|
|
413
|
-
cwd,
|
|
414
|
-
});
|
|
415
|
-
let memoryModPromise = null;
|
|
416
|
-
let searchModPromise = null;
|
|
417
|
-
let codeGraphModPromise = null;
|
|
418
|
-
|
|
419
|
-
// Memory module is always-on. `memoryEnabled()` is kept as a thin alias that
|
|
420
|
-
// now always returns true (callers/compaction helpers still reference it);
|
|
421
|
-
// the user-facing toggle is `recap` (background cycles only), read via
|
|
422
|
-
// recapEnabled(config).
|
|
423
|
-
const memoryEnabled = () => true;
|
|
424
|
-
const recapEnabledFn = () => recapEnabled(config, true);
|
|
425
|
-
const channelsEnabled = () => moduleEnabled(config, 'channels', true);
|
|
426
|
-
|
|
427
|
-
async function getMemoryModule() {
|
|
428
|
-
const startedAt = performance.now();
|
|
429
|
-
memoryModPromise ??= Promise.resolve(memoryRuntime);
|
|
430
|
-
const mod = await memoryModPromise;
|
|
431
|
-
if (typeof mod?.init === 'function') {
|
|
432
|
-
await mod.init();
|
|
433
|
-
}
|
|
434
|
-
bootProfile('memory-runtime:ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
435
|
-
return mod;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
async function getSearchModule() {
|
|
439
|
-
const startedAt = performance.now();
|
|
440
|
-
searchModPromise ??= import(SEARCH_RUNTIME);
|
|
441
|
-
const mod = await searchModPromise;
|
|
442
|
-
bootProfile('search-runtime:ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
443
|
-
return mod;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
async function getCodeGraphModule() {
|
|
447
|
-
const startedAt = performance.now();
|
|
448
|
-
codeGraphModPromise ??= import(CODE_GRAPH_RUNTIME);
|
|
449
|
-
const mod = await codeGraphModPromise;
|
|
450
|
-
bootProfile('code-graph-runtime:ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
451
|
-
return mod;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function persistLeadRoute(routeLike) {
|
|
455
|
-
const leadRoute = normalizeWorkflowRoute(routeLike);
|
|
456
|
-
if (!leadRoute) return null;
|
|
457
|
-
|
|
458
|
-
const nextConfig = { ...(config || {}) };
|
|
459
|
-
nextConfig.presets = upsertWorkflowPreset(nextConfig.presets, 'lead', leadRoute);
|
|
460
|
-
nextConfig.workflowRoutes = {
|
|
461
|
-
...(nextConfig.workflowRoutes || {}),
|
|
462
|
-
lead: leadRoute,
|
|
463
|
-
};
|
|
464
|
-
nextConfig.default = workflowPresetId('lead');
|
|
465
|
-
|
|
466
|
-
saveConfigAndAdopt(nextConfig);
|
|
467
|
-
return leadRoute;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
async function closePatchRuntimeIfLoaded(options = {}) {
|
|
471
|
-
const closer = globalThis.__mixdogCloseNativePatchServers;
|
|
472
|
-
if (typeof closer !== 'function' || globalThis.__mixdogNativePatchRuntimeTouched !== true) return;
|
|
473
|
-
bootProfile('patch-runtime:close:start');
|
|
474
|
-
const startedAt = performance.now();
|
|
475
|
-
try {
|
|
476
|
-
await closer(options);
|
|
477
|
-
} catch {
|
|
478
|
-
// Best-effort shutdown only; terminal restore must continue.
|
|
479
|
-
} finally {
|
|
480
|
-
bootProfile('patch-runtime:close:done', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const configStartedAt = performance.now();
|
|
485
|
-
let config = cfgMod.loadConfig({ secrets: false });
|
|
486
|
-
setConfiguredShell(normalizeSystemShellConfig(config.shell).command);
|
|
487
|
-
let configHasSecrets = false;
|
|
488
|
-
let route = resolveRoute(config, { provider, model });
|
|
489
|
-
let searchRoute = normalizeSearchRouteConfig(config.searchRoute);
|
|
490
|
-
bootProfile('config:ready', { ms: (performance.now() - configStartedAt).toFixed(1) });
|
|
491
|
-
let mode = normalizeToolMode(toolMode);
|
|
492
|
-
let session = null;
|
|
493
|
-
let sessionCreatePromise = null;
|
|
494
|
-
let currentCwd = cwd;
|
|
495
|
-
let sessionNeedsCwdRefresh = false;
|
|
496
|
-
// session_manage tool: reset request scheduled by the model mid-turn,
|
|
497
|
-
// consumed by the TUI engine at turn end ('clear' | 'compact_clear').
|
|
498
|
-
let pendingSessionReset = null;
|
|
499
|
-
let closeRequested = false;
|
|
500
|
-
const warmupTimers = {
|
|
501
|
-
providerSetupWarmupTimer: null,
|
|
502
|
-
providerWarmupTimer: null,
|
|
503
|
-
providerModelWarmupTimer: null,
|
|
504
|
-
modelCatalogWarmupTimer: null,
|
|
505
|
-
statuslineUsageWarmupTimer: null,
|
|
506
|
-
statuslineUsageRefreshTimer: null,
|
|
507
|
-
};
|
|
508
|
-
// Prewarm/channel-start timer handles + async state, owned here so the
|
|
509
|
-
// teardown clearTimeout sweep still sees them; the prewarm scheduler factory
|
|
510
|
-
// mutates these objects in place (see createPrewarmSchedulers).
|
|
511
|
-
const prewarmTimers = {
|
|
512
|
-
codeGraphPrewarmTimer: null,
|
|
513
|
-
channelStartTimer: null,
|
|
514
|
-
};
|
|
515
|
-
const prewarmState = {
|
|
516
|
-
codeGraphPrewarmInFlight: false,
|
|
517
|
-
codeGraphPrewarmQueuedCwd: '',
|
|
518
|
-
channelStartPromise: null,
|
|
519
|
-
};
|
|
520
|
-
let activeTurnCount = 0;
|
|
521
|
-
let firstTurnCompleted = false;
|
|
522
|
-
function hookTranscriptPath(sessionId) {
|
|
523
|
-
const id = clean(sessionId);
|
|
524
|
-
if (!id || !/^[A-Za-z0-9_-]+$/.test(id)) return null;
|
|
525
|
-
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
526
|
-
return join(dataDir, 'sessions', `${id}.json`);
|
|
527
|
-
}
|
|
528
|
-
function hookEffortPayload() {
|
|
529
|
-
const level = clean(route.effectiveEffort || route.effort);
|
|
530
|
-
return level ? { level: level.toLowerCase() } : undefined;
|
|
531
|
-
}
|
|
532
|
-
function hookCommonPayload(extra = {}) {
|
|
533
|
-
const sid = clean(extra.session_id || extra.sessionId || session?.id);
|
|
534
|
-
return {
|
|
535
|
-
...(sid ? { session_id: sid, transcript_path: hookTranscriptPath(sid) } : {}),
|
|
536
|
-
cwd: currentCwd,
|
|
537
|
-
permission_mode: session?.permissionMode || 'default',
|
|
538
|
-
...(hookEffortPayload() ? { effort: hookEffortPayload() } : {}),
|
|
539
|
-
...extra,
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
const sessionPrewarmDelayMs = envDelayMs('MIXDOG_SESSION_PREWARM_DELAY_MS', 50, { min: 0, max: 10_000 });
|
|
543
|
-
const providerSetupWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_SETUP_WARMUP_DELAY_MS', 300, { min: 0, max: 60_000 });
|
|
544
|
-
const modelCatalogWarmupDelayMs = envDelayMs('MIXDOG_MODEL_CATALOG_WARMUP_DELAY_MS', 200, { min: 0, max: 60_000 });
|
|
545
|
-
const providerWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_WARMUP_DELAY_MS', 1_500, { min: 0, max: 60_000 });
|
|
546
|
-
// Background model-catalog prefetch delay. Kept short so the first `/model`
|
|
547
|
-
// open finds a warm cache instead of paying a cold full network load. The
|
|
548
|
-
// work is async + unref'd, so short-lived detached runtimes still exit
|
|
549
|
-
// cleanly without waiting on it. Operators can raise it via env if a
|
|
550
|
-
// detached runtime must avoid the /models round-trip entirely.
|
|
551
|
-
const providerModelWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS', 2_000, { min: 0, max: 120_000 });
|
|
552
|
-
const codeGraphPrewarmDelayMs = envDelayMs('MIXDOG_CODE_GRAPH_PREWARM_DELAY_MS', 250, { min: 0, max: 60_000 });
|
|
553
|
-
const statuslineUsageWarmupDelayMs = envDelayMs('MIXDOG_STATUSLINE_USAGE_WARMUP_DELAY_MS', 800, { min: 0, max: 60_000 });
|
|
554
|
-
// Idle keep-alive: re-fetch usage before the statusline's 10-min staleness cut
|
|
555
|
-
// (LIVE_USAGE_SNAPSHOT_MAX_AGE_MS) so the usage segment does not disappear
|
|
556
|
-
// while the session sits idle with no turns to trigger a refresh.
|
|
557
|
-
const statuslineUsageRefreshDelayMs = envDelayMs('MIXDOG_STATUSLINE_USAGE_REFRESH_MS', 240_000, { min: 30_000, max: 540_000 });
|
|
558
|
-
const channelStartDelayMs = envDelayMs('MIXDOG_CHANNEL_START_DELAY_MS', 10_000, { min: 0, max: 120_000 });
|
|
559
|
-
const backgroundBusyRetryMs = envDelayMs('MIXDOG_BACKGROUND_BUSY_RETRY_MS', 1_000, { min: 50, max: 10_000 });
|
|
560
|
-
const sessionPrewarmEnabled = !envFlag('MIXDOG_DISABLE_SESSION_PREWARM')
|
|
561
|
-
&& (envFlag('MIXDOG_ENABLE_SESSION_PREWARM') || envPresent('MIXDOG_SESSION_PREWARM_DELAY_MS'));
|
|
562
|
-
const providerWarmupEnabled = !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
|
|
563
|
-
&& (
|
|
564
|
-
envFlag('MIXDOG_ENABLE_PROVIDER_WARMUP')
|
|
565
|
-
|| envFlag('MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN')
|
|
566
|
-
|| envPresent('MIXDOG_PROVIDER_WARMUP_DELAY_MS')
|
|
567
|
-
|| envPresent('MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS')
|
|
568
|
-
);
|
|
569
|
-
// Boot-time model-catalog prefetch is intentionally decoupled from the
|
|
570
|
-
// heavier providerWarmupEnabled gate (which stays opt-in for provider
|
|
571
|
-
// *init* side effects). Fetching the model list in the background after a
|
|
572
|
-
// short delay is cheap, fire-and-forget, and unref'd, so it is ON by
|
|
573
|
-
// default — otherwise the FIRST `/model` open always paid a cold full
|
|
574
|
-
// network load. Operators can still disable it explicitly.
|
|
575
|
-
const modelPrefetchEnabled = !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
|
|
576
|
-
&& !envFlag('MIXDOG_DISABLE_MODEL_PREFETCH');
|
|
577
|
-
const codeGraphPrewarmEnabled = !envFlag('MIXDOG_DISABLE_CODE_GRAPH_PREWARM');
|
|
578
|
-
const modelCatalogWarmupEnabled = !envFlag('MIXDOG_DISABLE_MODEL_CATALOG_WARMUP');
|
|
579
|
-
// Lazy code-graph prewarm (default ON): do NOT prewarm at startup / on cwd
|
|
580
|
-
// change — that fired ~250ms after the first frame and, in a large tree,
|
|
581
|
-
// burned a worker (and felt like a freeze) before the user did anything.
|
|
582
|
-
// Instead prewarm ONCE on the first real turn, when a code lookup is actually
|
|
583
|
-
// imminent. Operators who want the old eager behavior can set
|
|
584
|
-
// MIXDOG_CODE_GRAPH_PREWARM_EAGER=1.
|
|
585
|
-
const codeGraphPrewarmLazy = codeGraphPrewarmEnabled && !envFlag('MIXDOG_CODE_GRAPH_PREWARM_EAGER');
|
|
586
|
-
let codeGraphFirstTurnPrewarmDone = false;
|
|
587
|
-
const modelMetaByRoute = new Map();
|
|
588
|
-
const notificationListeners = new Set();
|
|
589
|
-
// Remote seat listeners (TUI): fired when remote mode flips outside a direct
|
|
590
|
-
// user action — currently only the superseded (seat stolen) path.
|
|
591
|
-
const remoteStateListeners = new Set();
|
|
592
|
-
function emitRemoteStateChange(enabled, reason = '') {
|
|
593
|
-
for (const listener of [...remoteStateListeners]) {
|
|
594
|
-
try { listener({ enabled: enabled === true, reason: String(reason || '') }); } catch {}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
const providerModelCaches = {
|
|
598
|
-
providerModelsCache: { models: null, at: 0 },
|
|
599
|
-
providerModelsPromise: null,
|
|
600
|
-
providerModelsLoadSeq: 0,
|
|
601
|
-
searchProviderModelsCache: { models: null, at: 0 },
|
|
602
|
-
};
|
|
603
|
-
const providerUsageCaches = {
|
|
604
|
-
usageDashboardCache: { dashboard: null, at: 0 },
|
|
605
|
-
usageDashboardPromise: null,
|
|
606
|
-
providerSetupCache: { setup: null, at: 0 },
|
|
607
|
-
providerSetupQuickCache: { setup: null, at: 0 },
|
|
608
|
-
providerSetupPromise: null,
|
|
609
|
-
};
|
|
610
|
-
let providerInitPromise = null;
|
|
611
|
-
let lastProjectMcpKey = null;
|
|
612
|
-
// MCP connect state, owned here so teardown/reconnect paths still observe it;
|
|
613
|
-
// the mcp-glue factory mutates this object in place (see createMcpGlue).
|
|
614
|
-
const mcpState = {
|
|
615
|
-
mcpFailures: [],
|
|
616
|
-
mcpConnectGeneration: 0,
|
|
617
|
-
mcpConnectInFlight: null,
|
|
618
|
-
};
|
|
619
|
-
// MCP glue factory — config/currentCwd live-bound; connect state shared via
|
|
620
|
-
// the caller-owned mcpState object above.
|
|
621
|
-
const {
|
|
622
|
-
mcpTransportLabel,
|
|
623
|
-
resolveEffectiveMcpServers,
|
|
624
|
-
mcpStatus,
|
|
625
|
-
connectConfiguredMcp,
|
|
626
|
-
normalizeMcpServerInput,
|
|
627
|
-
} = createMcpGlue({
|
|
628
|
-
mcpClient,
|
|
629
|
-
getConfig: () => config,
|
|
630
|
-
getCurrentCwd: () => currentCwd,
|
|
631
|
-
state: mcpState,
|
|
632
|
-
});
|
|
633
|
-
let preSessionToolSurface = null;
|
|
634
|
-
let contextStatusCacheKey = null;
|
|
635
|
-
let contextStatusCacheValue = null;
|
|
636
|
-
const hooksStartedAt = performance.now();
|
|
637
|
-
const hooks = createStandaloneHookBus({ dataDir: cfgMod.getPluginData() });
|
|
638
|
-
hooks.emit('runtime:start', { cwd: currentCwd, provider: route.provider, model: route.model, toolMode: mode });
|
|
639
|
-
bootProfile('hooks:ready', { ms: (performance.now() - hooksStartedAt).toFixed(1) });
|
|
640
|
-
|
|
641
|
-
// ---------------------------------------------------------------------
|
|
642
|
-
// Self-update (npm registry version check + optional auto-install).
|
|
643
|
-
// updateCheckState mirrors the last-known checkLatestVersion() result;
|
|
644
|
-
// updateProcessState tracks the in-flight install lifecycle so the TUI can
|
|
645
|
-
// poll getUpdateStatus() instead of needing a push/event channel. Both are
|
|
646
|
-
// purely in-memory (per runtime instance) — the on-disk cache lives inside
|
|
647
|
-
// update-checker.mjs and is what actually enforces the 24h TTL.
|
|
648
|
-
let updateCheckState = {
|
|
649
|
-
currentVersion: null,
|
|
650
|
-
latestVersion: null,
|
|
651
|
-
updateAvailable: false,
|
|
652
|
-
lastCheckedAt: 0,
|
|
653
|
-
};
|
|
654
|
-
// phase: 'idle' | 'checking' | 'installing' | 'installed' | 'failed'
|
|
655
|
-
let updateProcessState = { phase: 'idle', version: null, error: null };
|
|
656
|
-
|
|
657
|
-
function autoUpdateEnabled() {
|
|
658
|
-
return config?.update?.auto !== false;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
async function checkForUpdateInternal({ force = false } = {}) {
|
|
662
|
-
if (updateProcessState.phase !== 'installing') updateProcessState.phase = 'checking';
|
|
663
|
-
try {
|
|
664
|
-
const result = await checkLatestVersion({ force, dataDir: cfgMod.getPluginData?.() || STANDALONE_DATA_DIR });
|
|
665
|
-
updateCheckState = {
|
|
666
|
-
currentVersion: result.currentVersion,
|
|
667
|
-
latestVersion: result.latestVersion,
|
|
668
|
-
updateAvailable: result.updateAvailable,
|
|
669
|
-
lastCheckedAt: result.lastCheckedAt,
|
|
670
|
-
};
|
|
671
|
-
} catch {
|
|
672
|
-
// checkLatestVersion() is already silent-safe; this catch is belt-and-
|
|
673
|
-
// braces so a boot-time call can never crash the runtime.
|
|
674
|
-
} finally {
|
|
675
|
-
if (updateProcessState.phase === 'checking') updateProcessState.phase = 'idle';
|
|
676
|
-
}
|
|
677
|
-
return updateCheckState;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
async function runUpdateNowInternal() {
|
|
681
|
-
if (updateProcessState.phase === 'installing') {
|
|
682
|
-
return { ...updateProcessState, alreadyInstalling: true, error: 'update already in progress' };
|
|
683
|
-
}
|
|
684
|
-
updateProcessState = { phase: 'installing', version: null, error: null };
|
|
685
|
-
try {
|
|
686
|
-
const result = await runGlobalUpdate();
|
|
687
|
-
if (result?.ok) {
|
|
688
|
-
updateProcessState = { phase: 'installed', version: result.version || null, error: null };
|
|
689
|
-
} else {
|
|
690
|
-
updateProcessState = { phase: 'failed', version: null, error: result?.error || 'update failed' };
|
|
691
|
-
}
|
|
692
|
-
} catch (err) {
|
|
693
|
-
updateProcessState = { phase: 'failed', version: null, error: err?.message || String(err) };
|
|
694
|
-
}
|
|
695
|
-
return updateProcessState;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// Non-blocking boot hook: fires after the runtime object below is fully
|
|
699
|
-
// constructed (setTimeout(0) defers past the synchronous return), so a
|
|
700
|
-
// slow/hanging registry request or npm install can never delay session
|
|
701
|
-
// boot. Auto-update defaults to ON unless config.update.auto is explicitly
|
|
702
|
-
// false; a failed check or install is
|
|
703
|
-
// swallowed — getUpdateStatus()/getUpdateSettings() are the only surfaces,
|
|
704
|
-
// there is no push notice channel from runtime -> TUI today.
|
|
705
|
-
// force:true — always hit the registry at boot (the 24h disk cache went
|
|
706
|
-
// stale-visible: it kept reporting an older "latest" than the installed
|
|
707
|
-
// version). checkLatestVersion() still falls back to the cache offline.
|
|
708
|
-
const updateBootTimer = setTimeout(() => {
|
|
709
|
-
void (async () => {
|
|
710
|
-
await checkForUpdateInternal({ force: true });
|
|
711
|
-
if (autoUpdateEnabled() && updateCheckState.updateAvailable) {
|
|
712
|
-
await runUpdateNowInternal();
|
|
713
|
-
}
|
|
714
|
-
})().catch(() => {});
|
|
715
|
-
}, 0);
|
|
716
|
-
updateBootTimer.unref?.();
|
|
717
|
-
|
|
718
|
-
function emitRuntimeNotification(content, meta = {}) {
|
|
719
|
-
const text = String(content || '').trim();
|
|
720
|
-
if (!text) return false;
|
|
721
|
-
const event = { content: text, meta: meta && typeof meta === 'object' ? meta : {} };
|
|
722
|
-
let handled = false;
|
|
723
|
-
for (const listener of [...notificationListeners]) {
|
|
724
|
-
try {
|
|
725
|
-
if (listener(event) === true) handled = true;
|
|
726
|
-
} catch {}
|
|
727
|
-
}
|
|
728
|
-
return handled;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function notifyFnForSession(callerSessionId) {
|
|
732
|
-
return (text, meta = {}) => {
|
|
733
|
-
const handledByRuntimeListener = emitRuntimeNotification(text, meta);
|
|
734
|
-
let enqueued = false;
|
|
735
|
-
// TUI sessions consume raw execution notifications for UI/task cards via
|
|
736
|
-
// onNotification, but those raw envelopes are internal-only in pending
|
|
737
|
-
// drain. Always mirror terminal completions with a model-visible wrapper
|
|
738
|
-
// while keeping the raw text for UI display.
|
|
739
|
-
if (callerSessionId && typeof mgr.enqueuePendingMessage === 'function'
|
|
740
|
-
&& shouldPersistModelVisibleToolCompletion(text, meta)) {
|
|
741
|
-
try {
|
|
742
|
-
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
743
|
-
if (visible) enqueued = mgr.enqueuePendingMessage(callerSessionId, visible) > 0;
|
|
744
|
-
} catch {}
|
|
745
|
-
}
|
|
746
|
-
// Headless/API listeners may exist but not consume the event; preserve
|
|
747
|
-
// the old fallback for non-terminal notifications only when unhandled.
|
|
748
|
-
if (!enqueued && !handledByRuntimeListener && callerSessionId
|
|
749
|
-
&& typeof mgr.enqueuePendingMessage === 'function') {
|
|
750
|
-
try {
|
|
751
|
-
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
752
|
-
if (visible) enqueued = mgr.enqueuePendingMessage(callerSessionId, visible) > 0;
|
|
753
|
-
} catch {}
|
|
754
|
-
}
|
|
755
|
-
return enqueued || handledByRuntimeListener;
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function skillsStatus() {
|
|
760
|
-
const skills = typeof contextMod.collectSkillsCached === 'function'
|
|
761
|
-
? contextMod.collectSkillsCached(currentCwd)
|
|
762
|
-
: [];
|
|
763
|
-
const norm = (value) => String(value || '').replace(/\\/g, '/').toLowerCase();
|
|
764
|
-
const cwdNorm = norm(currentCwd);
|
|
765
|
-
const sourceForSkill = (filePath) => {
|
|
766
|
-
const path = norm(filePath);
|
|
767
|
-
if (cwdNorm && path.startsWith(`${cwdNorm}/.mixdog/skills/`)) return 'project';
|
|
768
|
-
return 'skill';
|
|
769
|
-
};
|
|
770
|
-
return {
|
|
771
|
-
cwd: currentCwd,
|
|
772
|
-
count: skills.length,
|
|
773
|
-
skills: skills.map((skill) => ({
|
|
774
|
-
name: skill.name,
|
|
775
|
-
description: skill.description || '',
|
|
776
|
-
filePath: skill.filePath || null,
|
|
777
|
-
source: sourceForSkill(skill.filePath),
|
|
778
|
-
})),
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// cwd resolution/apply + plugins-status + core-memory context. Extracted to
|
|
783
|
-
// session-runtime/cwd-plugins.mjs; the facade keeps ownership of the mutable
|
|
784
|
-
// currentCwd/session/config/lastProjectMcpKey locals via getter/setter
|
|
785
|
-
// injection and passes the later-defined callbacks (prewarm/tool-surface/
|
|
786
|
-
// memory) as closures.
|
|
787
|
-
const {
|
|
788
|
-
resolveCwdPath,
|
|
789
|
-
applyResolvedCwd,
|
|
790
|
-
refreshSessionForCwdIfNeeded,
|
|
791
|
-
pluginsStatus,
|
|
792
|
-
loadCoreMemoryContext,
|
|
793
|
-
} = createCwdPlugins({
|
|
794
|
-
getCurrentCwd: () => currentCwd,
|
|
795
|
-
setCurrentCwd: (next) => { currentCwd = next; },
|
|
796
|
-
getConfig: () => config,
|
|
797
|
-
getSession: () => session,
|
|
798
|
-
getRoute: () => route,
|
|
799
|
-
getLastProjectMcpKey: () => lastProjectMcpKey,
|
|
800
|
-
setLastProjectMcpKey: (next) => { lastProjectMcpKey = next; },
|
|
801
|
-
isCodeGraphPrewarmLazy: () => codeGraphPrewarmLazy,
|
|
802
|
-
isCodeGraphFirstTurnPrewarmDone: () => codeGraphFirstTurnPrewarmDone,
|
|
803
|
-
getCodeGraphPrewarmDelayMs: () => codeGraphPrewarmDelayMs,
|
|
804
|
-
setSessionNeedsCwdRefresh: (next) => { sessionNeedsCwdRefresh = next; },
|
|
805
|
-
connectConfiguredMcp,
|
|
806
|
-
invalidatePreSessionToolSurface: (...a) => invalidatePreSessionToolSurface(...a),
|
|
807
|
-
scheduleCodeGraphPrewarm: (...a) => scheduleCodeGraphPrewarm(...a),
|
|
808
|
-
hooks,
|
|
809
|
-
hookCommonPayload: (...a) => hookCommonPayload(...a),
|
|
810
|
-
bootProfile,
|
|
811
|
-
getMemoryModule: (...a) => getMemoryModule(...a),
|
|
812
|
-
listRegisteredPlugins,
|
|
813
|
-
pluginAdminStatus,
|
|
814
|
-
pluginManifest,
|
|
815
|
-
pluginMcpServerName,
|
|
816
|
-
mcpScriptForPlugin,
|
|
817
|
-
countSkillFiles,
|
|
818
|
-
readProjectMcpServers,
|
|
819
|
-
writeLastSessionCwd,
|
|
820
|
-
clean,
|
|
821
|
-
resolve,
|
|
822
|
-
statSync,
|
|
823
|
-
existsSync,
|
|
824
|
-
cfgMod,
|
|
825
|
-
STANDALONE_DATA_DIR,
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
function skillContent(name) {
|
|
829
|
-
const res = typeof contextMod.loadSkillResource === 'function'
|
|
830
|
-
? contextMod.loadSkillResource(name, currentCwd)
|
|
831
|
-
: null;
|
|
832
|
-
if (!res) throw new Error(`skill not found: ${name}`);
|
|
833
|
-
return { name, content: res.content, dir: res.dir };
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function skillToolContent(name) {
|
|
837
|
-
if (typeof contextMod.isSkillDisabled === 'function' && contextMod.isSkillDisabled(name)) {
|
|
838
|
-
const label = String(name || '').trim() || 'skill';
|
|
839
|
-
return `Error: skill "${label}" is disabled`;
|
|
840
|
-
}
|
|
841
|
-
const skill = skillContent(name);
|
|
842
|
-
// Return the general tool envelope so the main/Lead session behaves the
|
|
843
|
-
// same as agent-loop sessions: the model-visible tool_result is the short
|
|
844
|
-
// stub (`Loaded skill: <name>`) and the full SKILL.md body is delivered
|
|
845
|
-
// ONCE as a separate injected role:'user' message (newMessages). The
|
|
846
|
-
// envelope passes through internal-tools._normalize untouched (it preserves
|
|
847
|
-
// __toolEnvelope objects), and the agent loop's central normalizeToolEnvelope
|
|
848
|
-
// splits it into stub + injected user body.
|
|
849
|
-
return contextMod.buildSkillToolEnvelope(skill.name, skill.content, skill.dir);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function addProjectSkill(input = {}) {
|
|
853
|
-
const name = clean(input.name).replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
854
|
-
if (!name) throw new Error('skill name is required');
|
|
855
|
-
const dir = join(currentCwd, '.mixdog', 'skills', name);
|
|
856
|
-
const filePath = join(dir, 'SKILL.md');
|
|
857
|
-
if (existsSync(filePath)) throw new Error(`skill already exists: ${name}`);
|
|
858
|
-
const description = clean(input.description) || 'Project skill.';
|
|
859
|
-
mkdirSync(dir, { recursive: true });
|
|
860
|
-
writeFileSync(filePath, [
|
|
861
|
-
'---',
|
|
862
|
-
`name: ${name}`,
|
|
863
|
-
`description: ${description}`,
|
|
864
|
-
'---',
|
|
865
|
-
'',
|
|
866
|
-
'# Instructions',
|
|
867
|
-
'',
|
|
868
|
-
'Describe when and how to use this skill.',
|
|
869
|
-
'',
|
|
870
|
-
].join('\n'), 'utf8');
|
|
871
|
-
return { name, filePath };
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
const agentToolStartedAt = performance.now();
|
|
875
|
-
const agentTool = createStandaloneAgent({
|
|
876
|
-
cfgMod,
|
|
877
|
-
reg,
|
|
878
|
-
mgr,
|
|
879
|
-
dataDir: cfgMod.getPluginData(),
|
|
880
|
-
cwd,
|
|
881
|
-
// SubagentStart/SubagentStop: bridge internal worker spawn/finish to the
|
|
882
|
-
// standard hook bus. agent_type is passed top-level via hookCommonPayload
|
|
883
|
-
// (added to hook-bus buildEventPayload passthrough). Best-effort.
|
|
884
|
-
onSubagentEvent: (phase, info = {}) => {
|
|
885
|
-
try {
|
|
886
|
-
const event = phase === 'stop' ? 'SubagentStop' : 'SubagentStart';
|
|
887
|
-
void hooks.dispatch(event, hookCommonPayload({
|
|
888
|
-
session_id: info?.session_id || null,
|
|
889
|
-
agent_type: info?.agent_type || null,
|
|
890
|
-
}));
|
|
891
|
-
} catch { /* best-effort: subagent hook must never affect worker lifecycle */ }
|
|
892
|
-
},
|
|
893
|
-
});
|
|
894
|
-
bootProfile('agent:ready', { ms: (performance.now() - agentToolStartedAt).toFixed(1) });
|
|
895
|
-
const agentStatusState = () => {
|
|
896
|
-
try {
|
|
897
|
-
const status = agentTool.getStatus?.({ clientHostPid: session?.clientHostPid || process.pid }) || {};
|
|
898
|
-
return {
|
|
899
|
-
agentWorkers: Array.isArray(status.workers) ? status.workers : [],
|
|
900
|
-
agentJobs: Array.isArray(status.jobs) ? status.jobs : [],
|
|
901
|
-
agentScope: status.scope || null,
|
|
902
|
-
};
|
|
903
|
-
} catch {
|
|
904
|
-
return { agentWorkers: [], agentJobs: [], agentScope: null };
|
|
905
|
-
}
|
|
906
|
-
};
|
|
907
|
-
const channelsStartedAt = performance.now();
|
|
908
|
-
const channels = createStandaloneChannelWorker({
|
|
909
|
-
entry: join(STANDALONE_ROOT, CHANNEL_WORKER_ENTRY.replace(/^\.\//, '')),
|
|
910
|
-
rootDir: STANDALONE_ROOT,
|
|
911
|
-
dataDir: cfgMod.getPluginData(),
|
|
912
|
-
cwd,
|
|
913
|
-
onNotify: (msg) => {
|
|
914
|
-
// Single-holder remote: the worker reports it lost the bridge seat to a
|
|
915
|
-
// newer remote session. Drop remote mode entirely on this session (no
|
|
916
|
-
// handover, no retry) and tell UI listeners so the indicator updates.
|
|
917
|
-
if (msg?.method === 'notifications/mixdog/remote') {
|
|
918
|
-
if (msg?.params?.state === 'superseded' && remoteEnabled) {
|
|
919
|
-
stopRemote('superseded-by-newer-remote-session');
|
|
920
|
-
emitRemoteStateChange(false, 'superseded');
|
|
921
|
-
}
|
|
922
|
-
// Symmetric acquire: the worker took the bridge seat (boot make-before-
|
|
923
|
-
// break or activate). Flip remote ON via the same side-state a user
|
|
924
|
-
// /remote toggles — remoteEnabled + transcript writer — but WITHOUT
|
|
925
|
-
// re-invoking channel start (the worker is already running; startRemote
|
|
926
|
-
// would re-fork/activate). Idempotent: no-op when already enabled.
|
|
927
|
-
if (msg?.params?.state === 'acquired' && !remoteEnabled) {
|
|
928
|
-
remoteEnabled = true;
|
|
929
|
-
ensureRemoteTranscriptWriter();
|
|
930
|
-
emitRemoteStateChange(true, 'acquired');
|
|
931
|
-
}
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
if (msg?.method !== 'notifications/claude/channel') return;
|
|
935
|
-
const params = msg?.params && typeof msg.params === 'object' ? msg.params : {};
|
|
936
|
-
const meta = params.meta && typeof params.meta === 'object' ? params.meta : {};
|
|
937
|
-
const content = channelNotificationModelContent(params);
|
|
938
|
-
if (!content) return;
|
|
939
|
-
const handled = emitRuntimeNotification(content, meta);
|
|
940
|
-
if (!handled && session?.id && shouldMirrorChannelNotificationToPending(meta)) {
|
|
941
|
-
try { mgr.enqueuePendingMessage(session.id, content); } catch {}
|
|
942
|
-
}
|
|
943
|
-
},
|
|
944
|
-
});
|
|
945
|
-
bootProfile('channels:worker-ready', { ms: (performance.now() - channelsStartedAt).toFixed(1) });
|
|
946
|
-
const toolsStartedAt = performance.now();
|
|
947
|
-
const standaloneTools = [
|
|
948
|
-
TOOL_SEARCH_TOOL,
|
|
949
|
-
SKILL_TOOL,
|
|
950
|
-
CWD_TOOL,
|
|
951
|
-
SESSION_MANAGE_TOOL,
|
|
952
|
-
EXPLORE_TOOL,
|
|
953
|
-
...(searchToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'search' || tool?.name === 'web_fetch'),
|
|
954
|
-
...(memoryToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'recall' || tool?.name === 'memory'),
|
|
955
|
-
...(channelToolDefs?.TOOL_DEFS || []).filter((tool) => channels.isChannelTool(tool?.name)),
|
|
956
|
-
...(codeGraphToolDefs?.CODE_GRAPH_TOOL_DEFS || []).filter((tool) => tool?.name === 'code_graph'),
|
|
957
|
-
...agentTool.tools,
|
|
958
|
-
].map(applyStandaloneToolDefaults);
|
|
959
|
-
bootProfile('tools:ready', { ms: (performance.now() - toolsStartedAt).toFixed(1), count: standaloneTools.length });
|
|
960
|
-
|
|
961
|
-
function invalidatePreSessionToolSurface() {
|
|
962
|
-
preSessionToolSurface = null;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
function invalidateContextStatusCache() {
|
|
966
|
-
contextStatusCacheKey = null;
|
|
967
|
-
contextStatusCacheValue = null;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function buildPreSessionToolSurface() {
|
|
971
|
-
const previewTools = typeof mgr.previewSessionTools === 'function'
|
|
972
|
-
? mgr.previewSessionTools(toolSpecForMode(mode), [])
|
|
973
|
-
: [];
|
|
974
|
-
const tools = filterDisallowedTools(previewTools, LEAD_DISALLOWED_TOOLS);
|
|
975
|
-
const surface = { tools: Array.isArray(tools) ? tools.slice() : [] };
|
|
976
|
-
applyDeferredToolSurface(surface, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
977
|
-
return surface;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function activeToolSurface() {
|
|
981
|
-
if (session) return session;
|
|
982
|
-
preSessionToolSurface ??= buildPreSessionToolSurface();
|
|
983
|
-
return preSessionToolSurface;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function applyPreSessionToolSelection() {
|
|
987
|
-
if (!session || !preSessionToolSurface) return;
|
|
988
|
-
const selected = Array.isArray(preSessionToolSurface.deferredSelectedTools)
|
|
989
|
-
? preSessionToolSurface.deferredSelectedTools
|
|
990
|
-
: [];
|
|
991
|
-
const discovered = Array.isArray(preSessionToolSurface.deferredDiscoveredTools)
|
|
992
|
-
? preSessionToolSurface.deferredDiscoveredTools
|
|
993
|
-
: [];
|
|
994
|
-
const replay = [...new Set([...selected, ...discovered])];
|
|
995
|
-
if (replay.length) selectDeferredTools(session, replay, deferredSurfaceModeForLead(mode));
|
|
996
|
-
}
|
|
997
|
-
internalTools.setInternalToolsProvider({
|
|
998
|
-
tools: standaloneTools,
|
|
999
|
-
executor: async (name, args, callerCtx = {}) => {
|
|
1000
|
-
const callerCwd = callerCtx?.callerCwd || currentCwd;
|
|
1001
|
-
if (name === 'search' || name === 'web_fetch') {
|
|
1002
|
-
const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
|
|
1003
|
-
const searchMod = await getSearchModule();
|
|
1004
|
-
if (!searchMod?.handleToolCall) throw new Error('search runtime is not available');
|
|
1005
|
-
return await searchMod.handleToolCall(name, args || {}, {
|
|
1006
|
-
callerCwd,
|
|
1007
|
-
callerSessionId,
|
|
1008
|
-
routingSessionId: callerSessionId,
|
|
1009
|
-
clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
|
|
1010
|
-
notifyFn: notifyFnForSession(callerSessionId),
|
|
1011
|
-
nativeSearch: name === 'search'
|
|
1012
|
-
? async (searchArgs) => runNativeWebSearch(searchArgs, { signal: callerCtx?.signal || session?.controller?.signal })
|
|
1013
|
-
: undefined,
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1016
|
-
if (name === 'recall' || name === 'memory' || name === 'search_memories') {
|
|
1017
|
-
const memoryMod = await getMemoryModule();
|
|
1018
|
-
if (!memoryMod?.handleToolCall) throw new Error('memory runtime is not available');
|
|
1019
|
-
return await memoryMod.handleToolCall(name, args || {});
|
|
1020
|
-
}
|
|
1021
|
-
if (name === 'code_graph') {
|
|
1022
|
-
const codeGraphMod = await getCodeGraphModule();
|
|
1023
|
-
if (!codeGraphMod?.executeCodeGraphTool) throw new Error('code_graph runtime is not available');
|
|
1024
|
-
return await codeGraphMod.executeCodeGraphTool(name, args || {}, args?.cwd || callerCwd);
|
|
1025
|
-
}
|
|
1026
|
-
if (name === 'tool_search') {
|
|
1027
|
-
return renderToolSearch(args, activeToolSurface(), mode);
|
|
1028
|
-
}
|
|
1029
|
-
if (name === 'explore') {
|
|
1030
|
-
const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
|
|
1031
|
-
return await runExplore(args || {}, {
|
|
1032
|
-
callerCwd: args?.cwd ? resolveCwdPath(args.cwd) : callerCwd,
|
|
1033
|
-
callerSessionId,
|
|
1034
|
-
routingSessionId: callerSessionId,
|
|
1035
|
-
clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
|
|
1036
|
-
notifyFn: notifyFnForSession(callerSessionId),
|
|
1037
|
-
});
|
|
1038
|
-
}
|
|
1039
|
-
if (name === 'cwd') {
|
|
1040
|
-
const action = clean(args?.action || (args?.path ? 'set' : 'get')).toLowerCase();
|
|
1041
|
-
if (action === 'set') {
|
|
1042
|
-
applyResolvedCwd(resolveCwdPath(args?.path));
|
|
1043
|
-
} else if (action !== 'get') {
|
|
1044
|
-
throw new Error(`cwd: unknown action "${action}"`);
|
|
1045
|
-
}
|
|
1046
|
-
return JSON.stringify({ cwd: currentCwd, sessionId: session?.id || null }, null, 2);
|
|
1047
|
-
}
|
|
1048
|
-
if (name === 'session_manage') {
|
|
1049
|
-
// Lead/owner sessions only: an agent worker resetting its own
|
|
1050
|
-
// transcript mid-task would corrupt the delegation contract, and it
|
|
1051
|
-
// must never reach the owner conversation either.
|
|
1052
|
-
const callerSessionId = callerCtx?.callerSessionId || null;
|
|
1053
|
-
if (callerSessionId && session?.id && callerSessionId !== session.id) {
|
|
1054
|
-
throw new Error('session_manage: only the lead session may reset the conversation');
|
|
1055
|
-
}
|
|
1056
|
-
if (!session?.id) throw new Error('session_manage: no active session');
|
|
1057
|
-
const action = clean(args?.action).toLowerCase();
|
|
1058
|
-
if (action !== 'clear' && action !== 'compact_clear') {
|
|
1059
|
-
throw new Error(`session_manage: unknown action "${action}" (use clear | compact_clear)`);
|
|
1060
|
-
}
|
|
1061
|
-
// Never clear mid-turn — the loop is still reading the transcript.
|
|
1062
|
-
// Schedule and let the TUI engine consume it at turn end (same
|
|
1063
|
-
// boundary the idle auto-clear uses).
|
|
1064
|
-
// Pin to the current session id so a resume/new-session between
|
|
1065
|
-
// scheduling and consumption can never clear the wrong conversation.
|
|
1066
|
-
pendingSessionReset = { action, sessionId: session.id };
|
|
1067
|
-
return action === 'clear'
|
|
1068
|
-
? 'Session reset scheduled: full clear will run when this turn ends. All prior context will be gone.'
|
|
1069
|
-
: 'Session reset scheduled: the conversation will be summarized (compact) and cleared when this turn ends; key context carries forward in the summary.';
|
|
1070
|
-
}
|
|
1071
|
-
if (name === 'Skill') {
|
|
1072
|
-
return skillToolContent(args?.name);
|
|
1073
|
-
}
|
|
1074
|
-
if (name === 'agent') {
|
|
1075
|
-
const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
|
|
1076
|
-
return await agentTool.execute(args, {
|
|
1077
|
-
callerCwd,
|
|
1078
|
-
invocationSource: 'model-tool',
|
|
1079
|
-
callerSessionId,
|
|
1080
|
-
clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
|
|
1081
|
-
signal: callerCtx?.signal,
|
|
1082
|
-
notifyFn: notifyFnForSession(callerSessionId),
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
if (channels.isChannelTool(name)) {
|
|
1086
|
-
if (!channelsEnabled()) throw new Error('channels are disabled in settings');
|
|
1087
|
-
return await channels.execute(name, args || {});
|
|
1088
|
-
}
|
|
1089
|
-
throw new Error(`unknown standalone internal tool: ${name}`);
|
|
1090
|
-
},
|
|
1091
|
-
});
|
|
1092
|
-
internalTools.markBootReady?.();
|
|
1093
|
-
try { lastProjectMcpKey = resolve(currentCwd) + '\u0000' + JSON.stringify(readProjectMcpServers(currentCwd)); } catch { lastProjectMcpKey = null; }
|
|
1094
|
-
void connectConfiguredMcp()
|
|
1095
|
-
.then((status) => bootProfile('mcp:ready', {
|
|
1096
|
-
connected: Number(status?.connectedCount || 0),
|
|
1097
|
-
failed: Number(status?.failedCount || 0),
|
|
1098
|
-
}))
|
|
1099
|
-
.catch((error) => bootProfile('mcp:failed', { error: error?.message || String(error) }));
|
|
1100
|
-
|
|
1101
|
-
function reloadChannelsSoon() {
|
|
1102
|
-
channels.execute('reload_config', {}).catch(() => {});
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
function invalidateProviderCaches() {
|
|
1106
|
-
providerModelCaches.providerModelsCache = { models: null, at: 0 };
|
|
1107
|
-
providerModelCaches.providerModelsPromise = null;
|
|
1108
|
-
providerModelCaches.providerModelsLoadSeq += 1;
|
|
1109
|
-
providerModelCaches.searchProviderModelsCache = { models: null, at: 0 };
|
|
1110
|
-
providerUsageCaches.usageDashboardCache = { dashboard: null, at: 0 };
|
|
1111
|
-
providerUsageCaches.usageDashboardPromise = null;
|
|
1112
|
-
providerUsageCaches.providerSetupCache = { setup: null, at: 0 };
|
|
1113
|
-
providerUsageCaches.providerSetupQuickCache = { setup: null, at: 0 };
|
|
1114
|
-
providerUsageCaches.providerSetupPromise = null;
|
|
1115
|
-
providerInitPromise = null;
|
|
1116
|
-
modelMetaByRoute.clear();
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
// Config reload/save/adopt family + output-style status cache. Extracted to
|
|
1120
|
-
// session-runtime/config-lifecycle.mjs; the facade retains ownership of the
|
|
1121
|
-
// config/searchRoute/configHasSecrets mutable locals via getter/setter
|
|
1122
|
-
// injection (the proven mutable-state pattern).
|
|
1123
|
-
const {
|
|
1124
|
-
getOutputStyleStatusCached,
|
|
1125
|
-
invalidateOutputStyleStatusCache,
|
|
1126
|
-
seedOutputStyleStatusCache,
|
|
1127
|
-
adoptConfig,
|
|
1128
|
-
saveConfigAndAdopt,
|
|
1129
|
-
flushConfigSave,
|
|
1130
|
-
flushBackendSave,
|
|
1131
|
-
scheduleBackendSave,
|
|
1132
|
-
flushOutputStyleSave,
|
|
1133
|
-
scheduleOutputStyleSave,
|
|
1134
|
-
reloadFullConfig,
|
|
1135
|
-
ensureFullConfig,
|
|
1136
|
-
displayConfig,
|
|
1137
|
-
ensureConfigForRouteProvider,
|
|
1138
|
-
} = createConfigLifecycle({
|
|
1139
|
-
getConfig: () => config,
|
|
1140
|
-
setConfig: (next) => { config = next; },
|
|
1141
|
-
getSearchRoute: () => searchRoute,
|
|
1142
|
-
setSearchRoute: (next) => { searchRoute = next; },
|
|
1143
|
-
getConfigHasSecrets: () => configHasSecrets,
|
|
1144
|
-
setConfigHasSecrets: (next) => { configHasSecrets = next; },
|
|
1145
|
-
getRoute: () => route,
|
|
1146
|
-
cfgMod,
|
|
1147
|
-
sharedCfgMod,
|
|
1148
|
-
setBackend,
|
|
1149
|
-
setConfiguredShell,
|
|
1150
|
-
normalizeSystemShellConfig,
|
|
1151
|
-
normalizeSearchRouteConfig,
|
|
1152
|
-
outputStyleStatus,
|
|
1153
|
-
LAZY_SECRET_PROVIDERS,
|
|
1154
|
-
clean,
|
|
1155
|
-
resolve,
|
|
1156
|
-
STANDALONE_DATA_DIR,
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
async function ensureProvidersReady(providerConfig = config.providers || {}) {
|
|
1160
|
-
if (providerInitPromise) return await providerInitPromise;
|
|
1161
|
-
providerInitPromise = reg.initProviders(providerConfig)
|
|
1162
|
-
.finally(() => {
|
|
1163
|
-
providerInitPromise = null;
|
|
1164
|
-
});
|
|
1165
|
-
return await providerInitPromise;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
const {
|
|
1169
|
-
currentMainSearchModelMeta,
|
|
1170
|
-
runNativeWebSearch,
|
|
1171
|
-
} = createNativeSearch({
|
|
1172
|
-
getRoute: () => route,
|
|
1173
|
-
getSearchRoute: () => searchRoute,
|
|
1174
|
-
setSearchRoute: (next) => { searchRoute = next; },
|
|
1175
|
-
getConfig: () => config,
|
|
1176
|
-
getSession: () => session,
|
|
1177
|
-
getReg: () => reg,
|
|
1178
|
-
ensureFullConfig,
|
|
1179
|
-
ensureProvidersReady,
|
|
1180
|
-
ensureProviderEnabled,
|
|
1181
|
-
normalizeSearchProviderId,
|
|
1182
|
-
normalizeSearchRouteConfig,
|
|
1183
|
-
isDefaultSearchRouteConfig,
|
|
1184
|
-
isSearchCapableProvider,
|
|
1185
|
-
searchCapableFor,
|
|
1186
|
-
});
|
|
1187
|
-
|
|
1188
|
-
// Late-bound: createWarmupSchedulers is constructed after this factory, but
|
|
1189
|
-
// cachedProviderSetup(quick) may nudge scheduleProviderSetupWarmup on a cold
|
|
1190
|
-
// quick-cache fill. Thread it by reference so the scheduler is reachable once
|
|
1191
|
-
// it exists (a pre-scheduler quick fill simply skips the warmup nudge).
|
|
1192
|
-
let scheduleProviderSetupWarmupRef = () => {};
|
|
1193
|
-
const {
|
|
1194
|
-
refreshStatuslineUsageSnapshot,
|
|
1195
|
-
cachedProviderSetup,
|
|
1196
|
-
getUsageDashboard,
|
|
1197
|
-
} = createProviderUsage({
|
|
1198
|
-
caches: providerUsageCaches,
|
|
1199
|
-
getConfig: () => config,
|
|
1200
|
-
getReg: () => reg,
|
|
1201
|
-
displayConfig,
|
|
1202
|
-
providerSetup,
|
|
1203
|
-
createUsageDashboard,
|
|
1204
|
-
fetchOAuthUsageSnapshot,
|
|
1205
|
-
isCloseRequested: () => closeRequested,
|
|
1206
|
-
getProviderSetupWarmupTimer: () => warmupTimers.providerSetupWarmupTimer,
|
|
1207
|
-
scheduleProviderSetupWarmup: (delayMs) => scheduleProviderSetupWarmupRef(delayMs),
|
|
1208
|
-
});
|
|
1209
|
-
|
|
1210
|
-
// Holder filled after createQuickModelRows resolves; provider-models and
|
|
1211
|
-
// quick-model-rows are mutually dependent (rows need cache-row helpers, the
|
|
1212
|
-
// model factory needs quick fallbacks) so we thread the quick surface in by
|
|
1213
|
-
// reference after both are constructed.
|
|
1214
|
-
const providerModelQuickHelpers = {};
|
|
1215
|
-
// Late-bound: createWarmupSchedulers is constructed after this factory, but
|
|
1216
|
-
// lookupModelMeta may fire scheduleProviderModelWarmup on a cache miss. Thread
|
|
1217
|
-
// it by reference so the scheduler is called once it exists (miss handling is
|
|
1218
|
-
// best-effort; a pre-scheduler miss simply skips the warmup nudge).
|
|
1219
|
-
let scheduleProviderModelWarmupRef = () => {};
|
|
1220
|
-
const {
|
|
1221
|
-
modelMetaKey,
|
|
1222
|
-
lookupModelMeta,
|
|
1223
|
-
sortProviderModels,
|
|
1224
|
-
providerModelCacheRow,
|
|
1225
|
-
providerModelsFromCacheRows,
|
|
1226
|
-
collectSearchProviderModels,
|
|
1227
|
-
collectProviderModels,
|
|
1228
|
-
warmProviderModelCache,
|
|
1229
|
-
} = createProviderModels({
|
|
1230
|
-
caches: providerModelCaches,
|
|
1231
|
-
modelMetaByRoute,
|
|
1232
|
-
getRoute: () => route,
|
|
1233
|
-
getConfig: () => config,
|
|
1234
|
-
getReg: () => reg,
|
|
1235
|
-
searchCapableFor,
|
|
1236
|
-
sortProviderModelsRaw,
|
|
1237
|
-
providerModelCacheRowRaw,
|
|
1238
|
-
normalizeSearchProviderId,
|
|
1239
|
-
isSearchCapableProvider,
|
|
1240
|
-
ensureFullConfig,
|
|
1241
|
-
ensureProvidersReady,
|
|
1242
|
-
bootProfile,
|
|
1243
|
-
scheduleProviderModelWarmup: () => scheduleProviderModelWarmupRef(),
|
|
1244
|
-
quickHelpers: providerModelQuickHelpers,
|
|
1245
|
-
});
|
|
1246
|
-
|
|
1247
|
-
const {
|
|
1248
|
-
quickProviderModelRows,
|
|
1249
|
-
addDefaultSearchModel,
|
|
1250
|
-
quickSearchProviderModelRows,
|
|
1251
|
-
searchModelsFromRows,
|
|
1252
|
-
searchRowsWithDefault,
|
|
1253
|
-
} = createQuickModelRows({
|
|
1254
|
-
getRoute: () => route,
|
|
1255
|
-
getSearchRoute: () => searchRoute,
|
|
1256
|
-
displayConfig,
|
|
1257
|
-
providerModelCacheRow,
|
|
1258
|
-
providerModelsFromCacheRows,
|
|
1259
|
-
sortProviderModels,
|
|
1260
|
-
modelMetaByRoute,
|
|
1261
|
-
modelMetaKey,
|
|
1262
|
-
normalizeSearchProviderId,
|
|
1263
|
-
normalizeSearchRouteConfig,
|
|
1264
|
-
isSearchCapableProvider,
|
|
1265
|
-
searchCapableFor,
|
|
1266
|
-
currentMainSearchModelMeta,
|
|
1267
|
-
});
|
|
1268
|
-
Object.assign(providerModelQuickHelpers, {
|
|
1269
|
-
quickProviderModelRows,
|
|
1270
|
-
addDefaultSearchModel,
|
|
1271
|
-
quickSearchProviderModelRows,
|
|
1272
|
-
searchModelsFromRows,
|
|
1273
|
-
searchRowsWithDefault,
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
async function resolveMissingRouteModelForFirstTurn() {
|
|
1277
|
-
if (routeHasModel()) return route;
|
|
1278
|
-
const models = await collectProviderModels();
|
|
1279
|
-
const picked = models[0] || null;
|
|
1280
|
-
if (!picked) {
|
|
1281
|
-
throw new Error('No provider models available. Open /providers to sign in, then /model to choose a model.');
|
|
1282
|
-
}
|
|
1283
|
-
route = {
|
|
1284
|
-
...route,
|
|
1285
|
-
provider: picked.provider,
|
|
1286
|
-
model: picked.id,
|
|
1287
|
-
preset: null,
|
|
1288
|
-
};
|
|
1289
|
-
return route;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
async function refreshRouteEffort(modelMetaOverride = null) {
|
|
1293
|
-
await ensureProvidersReady(ensureProviderEnabled(config, route.provider));
|
|
1294
|
-
const modelMeta = modelMetaOverride || await lookupModelMeta(route.provider, route.model);
|
|
1295
|
-
const requested = hasOwn(route, 'effort') ? route.effort : (route.preset?.effort || null);
|
|
1296
|
-
const effectiveEffort = coerceEffortFor(route.provider, modelMeta, requested);
|
|
1297
|
-
const fastCapable = fastCapableFor(route.provider, modelMeta);
|
|
1298
|
-
// Carry the catalog display name onto the route so the statusline shows a
|
|
1299
|
-
// human label (e.g. "Claude Fable 5") for preset-less direct models instead
|
|
1300
|
-
// of the raw id. `name` is only trusted when it differs from the raw model
|
|
1301
|
-
// id (some providers echo the id as `name`), so it can't clobber a better
|
|
1302
|
-
// already-resolved label. Falls back to existing route.modelDisplay, then unset.
|
|
1303
|
-
const metaName = clean(modelMeta?.name);
|
|
1304
|
-
const modelDisplay = clean(modelMeta?.display) || clean(modelMeta?.displayName)
|
|
1305
|
-
|| (metaName && metaName !== clean(route.model) ? metaName : '')
|
|
1306
|
-
|| clean(route.modelDisplay);
|
|
1307
|
-
route = {
|
|
1308
|
-
...route,
|
|
1309
|
-
fast: fastCapable ? route.fast === true : false,
|
|
1310
|
-
fastCapable,
|
|
1311
|
-
effectiveEffort,
|
|
1312
|
-
effortOptions: effortItemsFor(route.provider, modelMeta, effectiveEffort),
|
|
1313
|
-
...(modelDisplay ? { modelDisplay } : {}),
|
|
1314
|
-
};
|
|
1315
|
-
return route;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
function routeHasModel() {
|
|
1319
|
-
return !!clean(route?.model);
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
function requireModelRoute() {
|
|
1323
|
-
if (routeHasModel()) return;
|
|
1324
|
-
throw new Error('No model configured. Open /providers to sign in, then /model to choose a model.');
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
async function recreateCurrentSessionIfReady() {
|
|
1328
|
-
if (!routeHasModel()) {
|
|
1329
|
-
session = null;
|
|
1330
|
-
return null;
|
|
1331
|
-
}
|
|
1332
|
-
return await createCurrentSession();
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
async function createCurrentSession(reason = 'demand') {
|
|
1336
|
-
if (sessionCreatePromise) return await sessionCreatePromise;
|
|
1337
|
-
if (session?.id && !sessionNeedsCwdRefresh) {
|
|
1338
|
-
const liveSession = mgr.getSession(session.id);
|
|
1339
|
-
if (liveSession && liveSession.closed !== true && liveSession.status !== 'closed') {
|
|
1340
|
-
session = liveSession;
|
|
1341
|
-
return session;
|
|
1342
|
-
}
|
|
1343
|
-
session = null;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
const startedAt = performance.now();
|
|
1347
|
-
bootProfile('session:create:start', { mode, reason });
|
|
1348
|
-
const promise = (async () => {
|
|
1349
|
-
ensureConfigForRouteProvider();
|
|
1350
|
-
await resolveMissingRouteModelForFirstTurn();
|
|
1351
|
-
requireModelRoute();
|
|
1352
|
-
bootProfile('session:create:route-ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
1353
|
-
// refreshRouteEffort (effort/model-meta) and loadCoreMemoryContext (memory
|
|
1354
|
-
// files) are independent — refreshRouteEffort only touches route effort/
|
|
1355
|
-
// display fields, never provider/model that the memory load reads — so run
|
|
1356
|
-
// them concurrently instead of serially on the boot path.
|
|
1357
|
-
const [, coreMemoryContext] = await Promise.all([
|
|
1358
|
-
refreshRouteEffort(),
|
|
1359
|
-
loadCoreMemoryContext(),
|
|
1360
|
-
]);
|
|
1361
|
-
bootProfile('session:create:effort-ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
1362
|
-
const providerImpl = reg.getProvider(route.provider);
|
|
1363
|
-
if (!providerImpl) {
|
|
1364
|
-
throw new Error(`Provider "${route.provider}" is not configured.`);
|
|
1365
|
-
}
|
|
1366
|
-
bootProfile('session:create:provider-ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
1367
|
-
if (closeRequested) throw new Error('runtime is closing');
|
|
1368
|
-
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
1369
|
-
// Load the active WORKFLOW.md pack once for both summary + context block.
|
|
1370
|
-
const { summary: workflow, context: workflowContext } = activeWorkflowContext(config, dataDir);
|
|
1371
|
-
const sessionOpts = {
|
|
1372
|
-
provider: route.provider,
|
|
1373
|
-
model: route.model,
|
|
1374
|
-
preset: route.preset || undefined,
|
|
1375
|
-
tools: toolSpecForMode(mode),
|
|
1376
|
-
owner: 'cli',
|
|
1377
|
-
agent: 'lead',
|
|
1378
|
-
lane: 'cli',
|
|
1379
|
-
sourceType: 'lead',
|
|
1380
|
-
sourceName: 'main',
|
|
1381
|
-
clientHostPid: process.pid,
|
|
1382
|
-
disallowedTools: LEAD_DISALLOWED_TOOLS,
|
|
1383
|
-
cwd: currentCwd,
|
|
1384
|
-
coreMemoryContext,
|
|
1385
|
-
workflow,
|
|
1386
|
-
workflowContext,
|
|
1387
|
-
fast: route.fast === true,
|
|
1388
|
-
compaction: config.compaction && typeof config.compaction === 'object'
|
|
1389
|
-
? normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() })
|
|
1390
|
-
: undefined,
|
|
1391
|
-
};
|
|
1392
|
-
if (hasOwn(route, 'effort') || route.effectiveEffort) {
|
|
1393
|
-
sessionOpts.effort = route.effectiveEffort || null;
|
|
1394
|
-
}
|
|
1395
|
-
session = mgr.createSession(sessionOpts);
|
|
1396
|
-
sessionNeedsCwdRefresh = false;
|
|
1397
|
-
attachSessionHooks(session, { hooks, hookCommonPayload, getCwd: () => currentCwd });
|
|
1398
|
-
applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
1399
|
-
applyPreSessionToolSelection();
|
|
1400
|
-
writeStatuslineRoute(statusRoutes, session, route);
|
|
1401
|
-
hooks.emit('session:create', { sessionId: session.id, provider: route.provider, model: route.model, toolMode: mode, cwd: currentCwd });
|
|
1402
|
-
// SessionStart: bridge to the standard project hook bus. Best-effort;
|
|
1403
|
-
// a hook error must never break session creation. additionalContext is
|
|
1404
|
-
// injected before the first user turn as a system-reminder context pair.
|
|
1405
|
-
try {
|
|
1406
|
-
const startSource = /resume/i.test(String(reason || ''))
|
|
1407
|
-
? 'resume'
|
|
1408
|
-
: (/clear/i.test(String(reason || '')) ? 'clear' : 'startup');
|
|
1409
|
-
const startDispatch = await hooks.dispatch('SessionStart', hookCommonPayload({ session_id: session.id, source: startSource, model: route.model }));
|
|
1410
|
-
const startContext = Array.isArray(startDispatch?.additionalContext)
|
|
1411
|
-
? startDispatch.additionalContext.join('\n\n')
|
|
1412
|
-
: String(startDispatch?.additionalContext || '');
|
|
1413
|
-
if (startContext.trim()) {
|
|
1414
|
-
session.messages.push({ role: 'user', content: `<system-reminder>\n# SessionStart Hook Context\n${startContext.trim()}\n</system-reminder>` });
|
|
1415
|
-
session.messages.push({ role: 'assistant', content: '.' });
|
|
1416
|
-
session.updatedAt = Date.now();
|
|
1417
|
-
}
|
|
1418
|
-
} catch { /* best-effort: never break session create */ }
|
|
1419
|
-
bootProfile('session:create:ready', {
|
|
1420
|
-
ms: (performance.now() - startedAt).toFixed(1),
|
|
1421
|
-
reason,
|
|
1422
|
-
tools: Array.isArray(session.tools) ? session.tools.length : 0,
|
|
1423
|
-
catalog: Array.isArray(session.deferredToolCatalog) ? session.deferredToolCatalog.length : 0,
|
|
1424
|
-
});
|
|
1425
|
-
return session;
|
|
1426
|
-
})();
|
|
1427
|
-
|
|
1428
|
-
sessionCreatePromise = promise;
|
|
1429
|
-
try {
|
|
1430
|
-
return await promise;
|
|
1431
|
-
} finally {
|
|
1432
|
-
if (sessionCreatePromise === promise) sessionCreatePromise = null;
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
const {
|
|
1437
|
-
scheduleProviderWarmup,
|
|
1438
|
-
scheduleProviderSetupWarmup,
|
|
1439
|
-
scheduleProviderModelWarmup,
|
|
1440
|
-
scheduleModelCatalogWarmup,
|
|
1441
|
-
scheduleStatuslineUsageWarmup,
|
|
1442
|
-
scheduleStatuslineUsageRefresh,
|
|
1443
|
-
} = createWarmupSchedulers({
|
|
1444
|
-
timers: warmupTimers,
|
|
1445
|
-
bootProfile,
|
|
1446
|
-
getRoute: () => route,
|
|
1447
|
-
getConfig: () => config,
|
|
1448
|
-
isCloseRequested: () => closeRequested,
|
|
1449
|
-
getActiveTurnCount: () => activeTurnCount,
|
|
1450
|
-
getSessionCreatePromise: () => sessionCreatePromise,
|
|
1451
|
-
getProviderModelsCache: () => providerModelCaches.providerModelsCache,
|
|
1452
|
-
getProviderModelsPromise: () => providerModelCaches.providerModelsPromise,
|
|
1453
|
-
reloadFullConfig,
|
|
1454
|
-
ensureConfigForRouteProvider,
|
|
1455
|
-
ensureProvidersReady,
|
|
1456
|
-
ensureProviderEnabled,
|
|
1457
|
-
refreshStatuslineUsageSnapshot,
|
|
1458
|
-
warmProviderModelCache,
|
|
1459
|
-
cachedProviderSetup,
|
|
1460
|
-
warmCatalogsInBackground,
|
|
1461
|
-
isFirstTurnCompleted: () => firstTurnCompleted,
|
|
1462
|
-
envFlag,
|
|
1463
|
-
delays: {
|
|
1464
|
-
providerWarmupDelayMs,
|
|
1465
|
-
providerSetupWarmupDelayMs,
|
|
1466
|
-
providerModelWarmupDelayMs,
|
|
1467
|
-
modelCatalogWarmupDelayMs,
|
|
1468
|
-
statuslineUsageWarmupDelayMs,
|
|
1469
|
-
statuslineUsageRefreshDelayMs,
|
|
1470
|
-
backgroundBusyRetryMs,
|
|
1471
|
-
},
|
|
1472
|
-
flags: {
|
|
1473
|
-
providerWarmupEnabled,
|
|
1474
|
-
modelPrefetchEnabled,
|
|
1475
|
-
modelCatalogWarmupEnabled,
|
|
1476
|
-
},
|
|
1477
|
-
});
|
|
1478
|
-
scheduleProviderModelWarmupRef = scheduleProviderModelWarmup;
|
|
1479
|
-
scheduleProviderSetupWarmupRef = scheduleProviderSetupWarmup;
|
|
1480
|
-
|
|
1481
|
-
const {
|
|
1482
|
-
scheduleCodeGraphPrewarm,
|
|
1483
|
-
scheduleLeadSessionPrewarm,
|
|
1484
|
-
invokeChannelStart,
|
|
1485
|
-
scheduleChannelStart,
|
|
1486
|
-
} = createPrewarmSchedulers({
|
|
1487
|
-
timers: prewarmTimers,
|
|
1488
|
-
bootProfile,
|
|
1489
|
-
getCurrentCwd: () => currentCwd,
|
|
1490
|
-
isCloseRequested: () => closeRequested,
|
|
1491
|
-
getActiveTurnCount: () => activeTurnCount,
|
|
1492
|
-
getSessionCreatePromise: () => sessionCreatePromise,
|
|
1493
|
-
getSession: () => session,
|
|
1494
|
-
isRemoteEnabled: () => remoteEnabled,
|
|
1495
|
-
channelsEnabled,
|
|
1496
|
-
getCodeGraphModule,
|
|
1497
|
-
createCurrentSession,
|
|
1498
|
-
channels,
|
|
1499
|
-
envFlag,
|
|
1500
|
-
delays: {
|
|
1501
|
-
codeGraphPrewarmDelayMs,
|
|
1502
|
-
channelStartDelayMs,
|
|
1503
|
-
sessionPrewarmDelayMs,
|
|
1504
|
-
backgroundBusyRetryMs,
|
|
1505
|
-
},
|
|
1506
|
-
flags: {
|
|
1507
|
-
codeGraphPrewarmEnabled,
|
|
1508
|
-
sessionPrewarmEnabled,
|
|
1509
|
-
},
|
|
1510
|
-
state: prewarmState,
|
|
1511
|
-
});
|
|
1512
|
-
|
|
1513
|
-
// Eagerly create/refresh the remote transcript writer for the CURRENT
|
|
1514
|
-
// session + cwd, publish the session record, and ensure the transcript
|
|
1515
|
-
// file exists on disk. Called from startRemote() (so the channel worker's
|
|
1516
|
-
// activate-time discovery finds THIS session immediately instead of
|
|
1517
|
-
// waiting for the first turn) and from ask() at turn start. Returns true
|
|
1518
|
-
// when a writer is bound.
|
|
1519
|
-
function ensureRemoteTranscriptWriter() {
|
|
1520
|
-
if (!remoteEnabled || !session?.id) return false;
|
|
1521
|
-
const twKey = `${session.id}\u0000${currentCwd}`;
|
|
1522
|
-
if (_twKey !== twKey) {
|
|
1523
|
-
try {
|
|
1524
|
-
_transcriptWriter = createTranscriptWriter({
|
|
1525
|
-
mixdogHome: mixdogHome(),
|
|
1526
|
-
sessionId: session.id,
|
|
1527
|
-
cwd: currentCwd,
|
|
1528
|
-
pid: process.pid,
|
|
1529
|
-
});
|
|
1530
|
-
_transcriptWriter.writeSessionRecord();
|
|
1531
|
-
_twKey = twKey;
|
|
1532
|
-
} catch (error) {
|
|
1533
|
-
process.stderr.write(`mixdog: transcript-writer: init failed: ${error?.message || error}\n`);
|
|
1534
|
-
_transcriptWriter = null;
|
|
1535
|
-
_twKey = '';
|
|
1536
|
-
return false;
|
|
1537
|
-
}
|
|
1538
|
-
} else {
|
|
1539
|
-
// Same binding — refresh updatedAt so worker-side discovery keeps
|
|
1540
|
-
// ranking this session as the live parent-chain candidate.
|
|
1541
|
-
try { _transcriptWriter?.writeSessionRecord(); } catch {}
|
|
1542
|
-
}
|
|
1543
|
-
try { _transcriptWriter?.ensureTranscriptFile(); }
|
|
1544
|
-
catch (error) { process.stderr.write(`mixdog: transcript-writer: ensureTranscriptFile failed: ${error?.message || error}\n`); }
|
|
1545
|
-
return _transcriptWriter != null;
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
// Remote (Discord channel) mode is opt-in per session. Only a session that
|
|
1549
|
-
// explicitly enables remote — via `mixdog --remote` or the runtime toggle —
|
|
1550
|
-
// boots the channel worker and contends for channel ownership.
|
|
1551
|
-
// startRemote() is FORCE-TAKEOVER: it always (re)claims the bridge seat and
|
|
1552
|
-
// rebinds output forwarding to this session, even when the worker is
|
|
1553
|
-
// already running (e.g. `/remote` re-issued after another session took the
|
|
1554
|
-
// seat, or to re-pin forwarding onto the current transcript).
|
|
1555
|
-
function startRemote() {
|
|
1556
|
-
remoteEnabled = true;
|
|
1557
|
-
// Boot the memory daemon eagerly. The channels worker forwards
|
|
1558
|
-
// transcript ingests/entries to the memory HTTP service, whose port is
|
|
1559
|
-
// published to active-instance.json by getMemoryModule().init(). Without
|
|
1560
|
-
// this, memory only starts on the first turn's getMemoryModule() call —
|
|
1561
|
-
// so early channel traffic finds no memory_port and gets buffered (or,
|
|
1562
|
-
// pre-drainer, silently dropped). Runs BEFORE channel claim so the port is
|
|
1563
|
-
// racing to be live by the time the worker sends its first ingest.
|
|
1564
|
-
//
|
|
1565
|
-
// Not fire-and-forget: init() only resolves the module handle, so a bare
|
|
1566
|
-
// getMemoryModule() could return before /health reports ok — leaving early
|
|
1567
|
-
// ingests to hit a not-yet-listening port. Await the proxy start() and then
|
|
1568
|
-
// poll /health with a bounded retry so the daemon is provably reachable
|
|
1569
|
-
// (or logged failed) rather than assumed-up. Still non-blocking to the
|
|
1570
|
-
// caller: the whole probe is detached, but it internally awaits readiness.
|
|
1571
|
-
void (async () => {
|
|
1572
|
-
try {
|
|
1573
|
-
// Yield one event-loop tick before the heavy chain below (module
|
|
1574
|
-
// resolve, daemon fork/health-poll) starts, so Ink's next render
|
|
1575
|
-
// (scheduled via setImmediate/timers) and any queued keypress
|
|
1576
|
-
// events get a turn first instead of being starved by this
|
|
1577
|
-
// detached chain's synchronous setup work.
|
|
1578
|
-
await new Promise((r) => setImmediate(r));
|
|
1579
|
-
const mod = await getMemoryModule();
|
|
1580
|
-
const started = typeof mod?.start === 'function' ? await mod.start() : null;
|
|
1581
|
-
const port = started?.port;
|
|
1582
|
-
if (!port) { bootProfile('channels:memory-eager-init-failed', { error: 'no port from start()' }); return; }
|
|
1583
|
-
for (let i = 0; i < 30; i++) {
|
|
1584
|
-
try {
|
|
1585
|
-
const ok = await new Promise((res) => {
|
|
1586
|
-
const req = httpMod.request({ hostname: '127.0.0.1', port, path: '/health', timeout: 1500 }, (r) => {
|
|
1587
|
-
let d = ''; r.on('data', (c) => { d += c; }); r.on('end', () => {
|
|
1588
|
-
try { res(JSON.parse(d)?.status === 'ok'); } catch { res(false); }
|
|
1589
|
-
});
|
|
1590
|
-
});
|
|
1591
|
-
req.on('error', () => res(false));
|
|
1592
|
-
req.on('timeout', () => { req.destroy(); res(false); });
|
|
1593
|
-
req.end();
|
|
1594
|
-
});
|
|
1595
|
-
if (ok) { bootProfile('channels:memory-eager-init-ready', { port }); return; }
|
|
1596
|
-
} catch {}
|
|
1597
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
1598
|
-
}
|
|
1599
|
-
bootProfile('channels:memory-eager-init-failed', { error: `health not ok after retries (port ${port})` });
|
|
1600
|
-
} catch (error) {
|
|
1601
|
-
bootProfile('channels:memory-eager-init-failed', { error: error?.message || String(error) });
|
|
1602
|
-
}
|
|
1603
|
-
})();
|
|
1604
|
-
// Publish this session's record + transcript file BEFORE the worker's
|
|
1605
|
-
// activate-time discovery polls, so output forwarding binds to this
|
|
1606
|
-
// terminal session immediately instead of waiting for the first turn.
|
|
1607
|
-
// No-op when the session has not been created yet (lazy mode); that
|
|
1608
|
-
// case is covered by the turn-start rebind in ask().
|
|
1609
|
-
ensureRemoteTranscriptWriter();
|
|
1610
|
-
// A backend switch may still be sitting in its debounce window; flush it
|
|
1611
|
-
// so the channel worker boots against the backend the user just chose,
|
|
1612
|
-
// not the previous on-disk value.
|
|
1613
|
-
try { flushBackendSave(); } catch {}
|
|
1614
|
-
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
1615
|
-
bootProfile('channels:start-skipped');
|
|
1616
|
-
return true;
|
|
1617
|
-
}
|
|
1618
|
-
if (!channelsEnabled()) {
|
|
1619
|
-
bootProfile('channels:start-disabled');
|
|
1620
|
-
return true;
|
|
1621
|
-
}
|
|
1622
|
-
if (closeRequested) return true;
|
|
1623
|
-
if (prewarmTimers.channelStartTimer) {
|
|
1624
|
-
clearTimeout(prewarmTimers.channelStartTimer);
|
|
1625
|
-
prewarmTimers.channelStartTimer = null;
|
|
1626
|
-
}
|
|
1627
|
-
bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
|
|
1628
|
-
void (async () => {
|
|
1629
|
-
// Yield before the createCurrentSession/transcript/fork chain below —
|
|
1630
|
-
// same rationale as the memory-eager-init yield above: this detached
|
|
1631
|
-
// chain runs synchronous config/fs work (createCurrentSession, backend
|
|
1632
|
-
// flush, transcript writer) back-to-back, and without a tick break it
|
|
1633
|
-
// can run ahead of Ink's queued render/input handling.
|
|
1634
|
-
await new Promise((r) => setImmediate(r));
|
|
1635
|
-
// Immediate-occupancy guarantee: make sure a session + transcript
|
|
1636
|
-
// exist BEFORE the worker boots — a freshly-forked worker claims and
|
|
1637
|
-
// runs transcript discovery inside its own start(), so publishing the
|
|
1638
|
-
// session record/file first lets that very first discovery pass bind
|
|
1639
|
-
// output forwarding to THIS terminal instead of a persisted/stale
|
|
1640
|
-
// neighbour. Lazy mode means the session may not exist yet at /remote
|
|
1641
|
-
// time; create it here (idempotent — reuses a live session, joins an
|
|
1642
|
-
// in-flight create). On create failure we still claim: that matches
|
|
1643
|
-
// the pre-eager behavior (bind resolves on the first turn's rebind).
|
|
1644
|
-
try { await createCurrentSession('remote-start'); }
|
|
1645
|
-
catch (error) { bootProfile('channels:remote-session-create-failed', { error: error?.message || String(error) }); }
|
|
1646
|
-
ensureRemoteTranscriptWriter();
|
|
1647
|
-
// Re-check after the awaits above: stopRemote()/superseded or runtime
|
|
1648
|
-
// close may have landed mid-chain — do not boot/claim for a session
|
|
1649
|
-
// that already turned remote off.
|
|
1650
|
-
if (!remoteEnabled || closeRequested) return;
|
|
1651
|
-
await invokeChannelStart();
|
|
1652
|
-
if (!remoteEnabled || closeRequested) return;
|
|
1653
|
-
// Unconditional claim + forwarder rebind. A freshly-forked worker
|
|
1654
|
-
// already claims in its own start(), so this is idempotent there; the
|
|
1655
|
-
// already-running case is where it matters (last-wins seat overwrite +
|
|
1656
|
-
// transcript rebind onto this session).
|
|
1657
|
-
await channels.execute('activate_channel_bridge', { active: true });
|
|
1658
|
-
})().catch((error) => bootProfile('channels:claim-failed', { error: error?.message || String(error) }));
|
|
1659
|
-
return true;
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
function stopRemote(reason) {
|
|
1663
|
-
remoteEnabled = false;
|
|
1664
|
-
// Cancel any pending deferred start so it can't fire after remote is off.
|
|
1665
|
-
if (prewarmTimers.channelStartTimer) { clearTimeout(prewarmTimers.channelStartTimer); prewarmTimers.channelStartTimer = null; }
|
|
1666
|
-
channels.stop(reason || 'remote-disabled', { waitForExit: false }).catch(() => {});
|
|
1667
|
-
return true;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
function isRemoteEnabled() {
|
|
1671
|
-
return remoteEnabled;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
function withTeardownDeadline(promise, ms, fallback = false) {
|
|
1675
|
-
let timer = null;
|
|
1676
|
-
return Promise.race([
|
|
1677
|
-
Promise.resolve(promise),
|
|
1678
|
-
new Promise((resolve) => {
|
|
1679
|
-
timer = setTimeout(() => resolve(fallback), ms);
|
|
1680
|
-
}),
|
|
1681
|
-
]).finally(() => {
|
|
1682
|
-
if (timer) clearTimeout(timer);
|
|
1683
|
-
});
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
bootProfile('session-runtime:ready', {
|
|
1687
|
-
lazySession: true,
|
|
1688
|
-
prewarmSession: sessionPrewarmEnabled,
|
|
1689
|
-
providerWarmup: providerWarmupEnabled,
|
|
1690
|
-
codeGraphPrewarm: codeGraphPrewarmEnabled,
|
|
1691
|
-
});
|
|
1692
|
-
scheduleLeadSessionPrewarm();
|
|
1693
|
-
// Lazy mode (default): skip the startup prewarm entirely; the first turn
|
|
1694
|
-
// triggers it instead (see ask()). Eager mode keeps the old startup schedule.
|
|
1695
|
-
if (!codeGraphPrewarmLazy) {
|
|
1696
|
-
scheduleCodeGraphPrewarm(codeGraphPrewarmDelayMs, 'startup');
|
|
1697
|
-
} else {
|
|
1698
|
-
bootProfile('code-graph:prewarm-lazy', { reason: 'startup-deferred-to-first-turn' });
|
|
1699
|
-
}
|
|
1700
|
-
scheduleProviderSetupWarmup();
|
|
1701
|
-
scheduleModelCatalogWarmup();
|
|
1702
|
-
scheduleProviderWarmup();
|
|
1703
|
-
// Warm the provider model catalog in the background, but keep it on its own
|
|
1704
|
-
// delay so short-lived detached runtimes can exit before /models I/O starts.
|
|
1705
|
-
// Operators that want earlier catalog warming can lower
|
|
1706
|
-
// MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS explicitly.
|
|
1707
|
-
scheduleProviderModelWarmup();
|
|
1708
|
-
scheduleStatuslineUsageWarmup();
|
|
1709
|
-
// Channels are opt-in: only boot the worker when this session started in (or
|
|
1710
|
-
// was toggled into) remote mode. Non-remote sessions never contend for the
|
|
1711
|
-
// channel; see startRemote()/stopRemote() and the `/remote` toggle.
|
|
1712
|
-
// `remote.autoStart` in mixdog-config.json makes every session claim remote
|
|
1713
|
-
// at boot (same force-takeover semantics as `mixdog --remote` / `/remote`).
|
|
1714
|
-
// The flag lives in the TOP-LEVEL `remote` section of mixdog-config.json
|
|
1715
|
-
// (sibling of agent/ui/channels), not inside the agent section that
|
|
1716
|
-
// cfgMod.loadConfig returns — read it via the shared whole-file reader.
|
|
1717
|
-
// `config?.remote?.autoStart` is kept for back-compat with agent-section
|
|
1718
|
-
// placement.
|
|
1719
|
-
if (!remoteEnabled) {
|
|
1720
|
-
try {
|
|
1721
|
-
if (config?.remote?.autoStart === true
|
|
1722
|
-
|| sharedCfgMod?.readSection?.('remote')?.autoStart === true) {
|
|
1723
|
-
remoteEnabled = true;
|
|
1724
|
-
}
|
|
1725
|
-
} catch { /* unreadable config never blocks boot */ }
|
|
1726
|
-
}
|
|
1727
|
-
// Boot-time remote start (autoStart or --remote) is DEFERRED past the TUI's
|
|
1728
|
-
// first frame. startRemote() front-loads heavy work — memory daemon fork
|
|
1729
|
-
// (PG + forced ONNX embed warmup in the child), eager session create, and
|
|
1730
|
-
// the channel-worker fork — and running it inline here interleaves that
|
|
1731
|
-
// CPU/disk load with engine boot, visibly delaying the first ink frame by
|
|
1732
|
-
// seconds. The deferred timer reuses prewarmTimers.channelStartTimer so an
|
|
1733
|
-
// early /remote (startRemote clears it), stopRemote(), and close() all
|
|
1734
|
-
// cancel it through the existing clearTimeout paths. Runtime /remote calls
|
|
1735
|
-
// still start immediately (user-initiated, UI already painted).
|
|
1736
|
-
if (remoteEnabled) {
|
|
1737
|
-
const remoteAutoStartDelayMs = envDelayMs('MIXDOG_REMOTE_AUTOSTART_DELAY_MS', 1_500, { min: 0, max: 60_000 });
|
|
1738
|
-
bootProfile('channels:autostart-deferred', { delayMs: remoteAutoStartDelayMs });
|
|
1739
|
-
prewarmTimers.channelStartTimer = setTimeout(() => {
|
|
1740
|
-
prewarmTimers.channelStartTimer = null;
|
|
1741
|
-
if (closeRequested || !remoteEnabled) return;
|
|
1742
|
-
startRemote();
|
|
1743
|
-
}, remoteAutoStartDelayMs);
|
|
1744
|
-
prewarmTimers.channelStartTimer.unref?.();
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
function contextStatusCacheKeyFor({ messages, tools }) {
|
|
1748
|
-
const compaction = session?.compaction || {};
|
|
1749
|
-
const lastMessage = messages[messages.length - 1] || null;
|
|
1750
|
-
return {
|
|
1751
|
-
session,
|
|
1752
|
-
sessionId: session?.id || null,
|
|
1753
|
-
provider: route.provider,
|
|
1754
|
-
model: route.model,
|
|
1755
|
-
cwd: currentCwd,
|
|
1756
|
-
mode,
|
|
1757
|
-
messages,
|
|
1758
|
-
messageCount: messages.length,
|
|
1759
|
-
lastMessage,
|
|
1760
|
-
lastMessageRole: lastMessage?.role || null,
|
|
1761
|
-
lastMessageContent: lastMessage?.content || null,
|
|
1762
|
-
tools,
|
|
1763
|
-
toolCount: tools.length,
|
|
1764
|
-
contextWindow: session?.contextWindow || null,
|
|
1765
|
-
rawContextWindow: session?.rawContextWindow || null,
|
|
1766
|
-
effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
|
|
1767
|
-
autoCompactTokenLimit: Number(session?.autoCompactTokenLimit || 0),
|
|
1768
|
-
lastContextTokens: Number(session?.lastContextTokens || 0),
|
|
1769
|
-
lastContextTokensUpdatedAt: Number(session?.lastContextTokensUpdatedAt || 0),
|
|
1770
|
-
lastContextTokensStaleAfterCompact: session?.lastContextTokensStaleAfterCompact === true,
|
|
1771
|
-
lastInputTokens: Number(session?.lastInputTokens || 0),
|
|
1772
|
-
lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
|
|
1773
|
-
lastOutputTokens: Number(session?.lastOutputTokens || 0),
|
|
1774
|
-
lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
|
|
1775
|
-
lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
|
|
1776
|
-
totalInputTokens: Number(session?.totalInputTokens || 0),
|
|
1777
|
-
totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
|
|
1778
|
-
totalOutputTokens: Number(session?.totalOutputTokens || 0),
|
|
1779
|
-
totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
|
|
1780
|
-
totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
|
|
1781
|
-
compactBoundaryTokens: Number(session?.compactBoundaryTokens || 0),
|
|
1782
|
-
compactionBoundaryTokens: Number(compaction.boundaryTokens || 0),
|
|
1783
|
-
compactionTriggerTokens: Number(compaction.triggerTokens || 0),
|
|
1784
|
-
compactionLastChangedAt: Number(compaction.lastChangedAt || 0),
|
|
1785
|
-
compactionLastCompactAt: Number(compaction.lastCompactAt || 0),
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
function sameContextStatusCacheKey(a, b) {
|
|
1790
|
-
if (!a || !b) return false;
|
|
1791
|
-
for (const key of Object.keys(a)) {
|
|
1792
|
-
if (!Object.is(a[key], b[key])) return false;
|
|
1793
|
-
}
|
|
1794
|
-
return true;
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
// Pure settings-delegate methods (onboarding status/skip, autoClear, profile,
|
|
1798
|
-
// compaction, recap/memory, channels, systemShell, update settings, channel
|
|
1799
|
-
// token save/forget, setBackend). Extracted to session-runtime/settings-api.mjs
|
|
1800
|
-
// and SPREAD into the API object below so the external surface is unchanged.
|
|
1801
|
-
const settingsApi = createSettingsApi({
|
|
1802
|
-
getConfig: () => config,
|
|
1803
|
-
getRoute: () => route,
|
|
1804
|
-
getSession: () => session,
|
|
1805
|
-
getRemoteEnabled: () => remoteEnabled,
|
|
1806
|
-
adoptConfig,
|
|
1807
|
-
saveConfigAndAdopt,
|
|
1808
|
-
scheduleBackendSave,
|
|
1809
|
-
cfgMod,
|
|
1810
|
-
hasOwn,
|
|
1811
|
-
normalizeAutoClearConfig,
|
|
1812
|
-
autoClearIdleMsForProvider,
|
|
1813
|
-
autoClearProviderDefaults,
|
|
1814
|
-
normalizeCompactionConfig,
|
|
1815
|
-
normalizeCompactTypeSetting,
|
|
1816
|
-
normalizeSystemShellConfig,
|
|
1817
|
-
normalizeSystemShellCommand,
|
|
1818
|
-
setConfiguredShell,
|
|
1819
|
-
setRecapEnabledInConfig,
|
|
1820
|
-
setModuleEnabledInConfig,
|
|
1821
|
-
summarizeWorkflowRoutes,
|
|
1822
|
-
parseDurationMs,
|
|
1823
|
-
formatDurationMs,
|
|
1824
|
-
localPackageVersion,
|
|
1825
|
-
memoryEnabled,
|
|
1826
|
-
recapEnabledFn,
|
|
1827
|
-
channelsEnabled,
|
|
1828
|
-
autoUpdateEnabled,
|
|
1829
|
-
getUpdateCheckState: () => updateCheckState,
|
|
1830
|
-
getUpdateProcessState: () => updateProcessState,
|
|
1831
|
-
invalidateContextStatusCache: (...a) => invalidateContextStatusCache(...a),
|
|
1832
|
-
invalidatePreSessionToolSurface: (...a) => invalidatePreSessionToolSurface(...a),
|
|
1833
|
-
scheduleChannelStart: (...a) => scheduleChannelStart(...a),
|
|
1834
|
-
channels,
|
|
1835
|
-
clearChannelStartTimer: () => {
|
|
1836
|
-
if (prewarmTimers.channelStartTimer) {
|
|
1837
|
-
clearTimeout(prewarmTimers.channelStartTimer);
|
|
1838
|
-
prewarmTimers.channelStartTimer = null;
|
|
1839
|
-
}
|
|
1840
|
-
},
|
|
1841
|
-
checkForUpdateInternal: (...a) => checkForUpdateInternal(...a),
|
|
1842
|
-
runUpdateNowInternal: (...a) => runUpdateNowInternal(...a),
|
|
1843
|
-
reloadChannelsSoon: (...a) => reloadChannelsSoon(...a),
|
|
1844
|
-
ONBOARDING_VERSION,
|
|
1845
|
-
saveDiscordToken,
|
|
1846
|
-
forgetDiscordToken,
|
|
1847
|
-
saveTelegramToken,
|
|
1848
|
-
forgetTelegramToken,
|
|
1849
|
-
saveWebhookAuthtoken,
|
|
1850
|
-
forgetWebhookAuthtoken,
|
|
1851
|
-
setBackend,
|
|
1852
|
-
});
|
|
1853
|
-
|
|
1854
|
-
return {
|
|
1855
|
-
...settingsApi,
|
|
1856
|
-
get id() {
|
|
1857
|
-
return session?.id || null;
|
|
1858
|
-
},
|
|
1859
|
-
get provider() {
|
|
1860
|
-
return route.provider;
|
|
1861
|
-
},
|
|
1862
|
-
get model() {
|
|
1863
|
-
return route.model;
|
|
1864
|
-
},
|
|
1865
|
-
get effort() {
|
|
1866
|
-
return route.effectiveEffort || route.effort || route.preset?.effort || null;
|
|
1867
|
-
},
|
|
1868
|
-
get fast() {
|
|
1869
|
-
return route.fast === true;
|
|
1870
|
-
},
|
|
1871
|
-
get fastCapable() {
|
|
1872
|
-
return route.fastCapable === true;
|
|
1873
|
-
},
|
|
1874
|
-
get effortOptions() {
|
|
1875
|
-
return route.effortOptions || [];
|
|
1876
|
-
},
|
|
1877
|
-
get contextWindow() {
|
|
1878
|
-
return session?.contextWindow || null;
|
|
1879
|
-
},
|
|
1880
|
-
get rawContextWindow() {
|
|
1881
|
-
return session?.rawContextWindow || session?.contextWindow || null;
|
|
1882
|
-
},
|
|
1883
|
-
get effectiveContextWindowPercent() {
|
|
1884
|
-
return session?.effectiveContextWindowPercent || null;
|
|
1885
|
-
},
|
|
1886
|
-
get toolMode() {
|
|
1887
|
-
return mode;
|
|
1888
|
-
},
|
|
1889
|
-
get autoClear() {
|
|
1890
|
-
return this.getAutoClear();
|
|
1891
|
-
},
|
|
1892
|
-
get systemShell() {
|
|
1893
|
-
return normalizeSystemShellConfig(config.shell);
|
|
1894
|
-
},
|
|
1895
|
-
get searchRoute() {
|
|
1896
|
-
searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
1897
|
-
return searchRoute;
|
|
1898
|
-
},
|
|
1899
|
-
get workflow() {
|
|
1900
|
-
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
1901
|
-
const active = activeWorkflowSummary(config, dataDir);
|
|
1902
|
-
if (session?.workflow && typeof session.workflow === 'object') {
|
|
1903
|
-
const current = workflowSummary(session.workflow);
|
|
1904
|
-
return current?.id && active?.id && current.id !== active.id
|
|
1905
|
-
? { ...active, currentSession: current, appliedToCurrentSession: false }
|
|
1906
|
-
: active;
|
|
1907
|
-
}
|
|
1908
|
-
return active;
|
|
1909
|
-
},
|
|
1910
|
-
get outputStyle() {
|
|
1911
|
-
return getOutputStyleStatusCached().current;
|
|
1912
|
-
},
|
|
1913
|
-
get cwd() {
|
|
1914
|
-
return currentCwd;
|
|
1915
|
-
},
|
|
1916
|
-
get session() {
|
|
1917
|
-
return session;
|
|
1918
|
-
},
|
|
1919
|
-
contextStatus() {
|
|
1920
|
-
// Prefer the in-flight working transcript while a turn is running so the
|
|
1921
|
-
// context gauge reflects LIVE growth (user turn + tool calls/results) as
|
|
1922
|
-
// it accumulates, instead of freezing at the pre-turn committed snapshot.
|
|
1923
|
-
// askSession() sets session.liveTurnMessages for the turn duration and
|
|
1924
|
-
// clears it on commit/cancel/error, after which we fall back to the
|
|
1925
|
-
// authoritative committed transcript.
|
|
1926
|
-
const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
|
|
1927
|
-
const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
|
|
1928
|
-
const tools = Array.isArray(session?.tools) ? session.tools : [];
|
|
1929
|
-
const cacheKey = contextStatusCacheKeyFor({ messages, tools });
|
|
1930
|
-
if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
|
|
1931
|
-
return contextStatusCacheValue;
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
const messageSummary = summarizeContextMessages(messages);
|
|
1935
|
-
const toolSchemaTokens = estimateToolSchemaTokens(tools);
|
|
1936
|
-
const toolSchemaBreakdown = estimateToolSchemaBreakdown(tools);
|
|
1937
|
-
const requestReserveTokens = estimateRequestReserveTokens(tools);
|
|
1938
|
-
const requestOverheadTokens = Math.max(0, requestReserveTokens - toolSchemaTokens);
|
|
1939
|
-
const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
|
|
1940
|
-
const effectiveWindow = Number(session?.contextWindow || rawWindow || 0);
|
|
1941
|
-
const lastContextTokens = Number(session?.lastContextTokens || 0);
|
|
1942
|
-
const estimatedContextTokens = estimateTranscriptContextUsage(messages, tools, {
|
|
1943
|
-
messageCount: messageSummary.count,
|
|
1944
|
-
});
|
|
1945
|
-
const compactAt = Number(session?.compaction?.lastChangedAt || session?.compaction?.lastCompactAt || 0);
|
|
1946
|
-
const usageAt = Number(session?.lastContextTokensUpdatedAt || 0);
|
|
1947
|
-
const lastUsageStale = !!lastContextTokens && (
|
|
1948
|
-
session?.lastContextTokensStaleAfterCompact === true
|
|
1949
|
-
|| (compactAt > 0 && usageAt > 0 && usageAt <= compactAt)
|
|
1950
|
-
|| (compactAt > 0 && usageAt <= 0)
|
|
1951
|
-
);
|
|
1952
|
-
const compactBoundaryTokens = Number(session?.compactBoundaryTokens || session?.compaction?.boundaryTokens || 0);
|
|
1953
|
-
const displayWindow = compactBoundaryTokens || effectiveWindow;
|
|
1954
|
-
// The transcript estimate is the single source of truth for the displayed
|
|
1955
|
-
// context footprint. Provider-reported input_tokens (lastContextTokens)
|
|
1956
|
-
// swing non-monotonically and are not window-bounded on some providers
|
|
1957
|
-
// (e.g. OpenAI gpt-5.5 Responses API), so they are kept only as secondary
|
|
1958
|
-
// metadata (lastApiRequestTokens / usage.lastContextTokens) and never feed
|
|
1959
|
-
// the gauge numerator.
|
|
1960
|
-
const usedTokens = estimatedContextTokens;
|
|
1961
|
-
const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
|
|
1962
|
-
// Use the same shared compact-policy math as manager/loop. Do not trust
|
|
1963
|
-
// persisted trigger telemetry as an independent policy input: it is an
|
|
1964
|
-
// output snapshot and was the source of repeated /context false positives.
|
|
1965
|
-
const compactTriggerTokens = resolveCompactTriggerTokens(session || {}, compactBoundaryTokens) || 0;
|
|
1966
|
-
const compactBufferTokens = compactBoundaryTokens
|
|
1967
|
-
? Math.max(0, compactBoundaryTokens - compactTriggerTokens)
|
|
1968
|
-
: resolveCompactBufferTokens(compactBoundaryTokens, session?.compaction || {});
|
|
1969
|
-
const value = {
|
|
1970
|
-
sessionId: session?.id || null,
|
|
1971
|
-
provider: route.provider,
|
|
1972
|
-
model: route.model,
|
|
1973
|
-
cwd: currentCwd,
|
|
1974
|
-
toolMode: mode,
|
|
1975
|
-
contextWindow: displayWindow || effectiveWindow || null,
|
|
1976
|
-
effectiveContextWindow: effectiveWindow || null,
|
|
1977
|
-
rawContextWindow: rawWindow || null,
|
|
1978
|
-
effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
|
|
1979
|
-
usedTokens,
|
|
1980
|
-
usedSource: 'estimated',
|
|
1981
|
-
currentEstimatedTokens: estimatedContextTokens,
|
|
1982
|
-
lastApiRequestTokens: lastContextTokens || 0,
|
|
1983
|
-
lastApiRequestStale: lastUsageStale,
|
|
1984
|
-
freeTokens,
|
|
1985
|
-
compaction: {
|
|
1986
|
-
...(session?.compaction || {}),
|
|
1987
|
-
boundaryTokens: compactBoundaryTokens || null,
|
|
1988
|
-
triggerTokens: compactTriggerTokens || null,
|
|
1989
|
-
bufferTokens: compactBufferTokens || null,
|
|
1990
|
-
currentEstimatedTokens: estimatedContextTokens,
|
|
1991
|
-
lastApiRequestTokens: lastContextTokens || 0,
|
|
1992
|
-
lastApiRequestStale: lastUsageStale,
|
|
1993
|
-
},
|
|
1994
|
-
messages: messageSummary,
|
|
1995
|
-
request: {
|
|
1996
|
-
toolSchemaTokens,
|
|
1997
|
-
toolSchemaBreakdown,
|
|
1998
|
-
requestOverheadTokens,
|
|
1999
|
-
reserveTokens: requestReserveTokens,
|
|
2000
|
-
},
|
|
2001
|
-
usage: {
|
|
2002
|
-
lastInputTokens: Number(session?.lastInputTokens || 0),
|
|
2003
|
-
lastUncachedInputTokens: Number(session?.lastUncachedInputTokens || 0),
|
|
2004
|
-
lastOutputTokens: Number(session?.lastOutputTokens || 0),
|
|
2005
|
-
lastCachedReadTokens: Number(session?.lastCachedReadTokens || 0),
|
|
2006
|
-
lastCacheWriteTokens: Number(session?.lastCacheWriteTokens || 0),
|
|
2007
|
-
lastContextTokens,
|
|
2008
|
-
totalInputTokens: Number(session?.totalInputTokens || 0),
|
|
2009
|
-
totalUncachedInputTokens: Number(session?.totalUncachedInputTokens || 0),
|
|
2010
|
-
totalOutputTokens: Number(session?.totalOutputTokens || 0),
|
|
2011
|
-
totalCachedReadTokens: Number(session?.totalCachedReadTokens || 0),
|
|
2012
|
-
totalCacheWriteTokens: Number(session?.totalCacheWriteTokens || 0),
|
|
2013
|
-
},
|
|
2014
|
-
};
|
|
2015
|
-
contextStatusCacheKey = cacheKey;
|
|
2016
|
-
contextStatusCacheValue = value;
|
|
2017
|
-
return value;
|
|
2018
|
-
},
|
|
2019
|
-
listProviders() {
|
|
2020
|
-
return renderProviderStatus(displayConfig());
|
|
2021
|
-
},
|
|
2022
|
-
async getProviderSetup() {
|
|
2023
|
-
return await cachedProviderSetup();
|
|
2024
|
-
},
|
|
2025
|
-
async getUsageDashboard(options = {}) {
|
|
2026
|
-
return await getUsageDashboard(options);
|
|
2027
|
-
},
|
|
2028
|
-
async completeOnboarding(payload = {}) {
|
|
2029
|
-
// Only fall back to the live runtime route when the caller actually sent a
|
|
2030
|
-
// defaultRoute. The onboarding "partial save" path (Main left unset, only
|
|
2031
|
-
// Search/agent picks) omits defaultRoute entirely and must NOT persist the
|
|
2032
|
-
// current route as Main or recreate the session.
|
|
2033
|
-
const defaultRoute = hasOwn(payload, 'defaultRoute')
|
|
2034
|
-
? normalizeWorkflowRoute(payload.defaultRoute, route)
|
|
2035
|
-
: null;
|
|
2036
|
-
const workflowInput = payload.workflowRoutes && typeof payload.workflowRoutes === 'object'
|
|
2037
|
-
? payload.workflowRoutes
|
|
2038
|
-
: {};
|
|
2039
|
-
const nextConfig = { ...config };
|
|
2040
|
-
if (hasOwn(payload, 'defaultProvider')) {
|
|
2041
|
-
const requested = clean(payload.defaultProvider);
|
|
2042
|
-
if (requested) {
|
|
2043
|
-
if (!isKnownProvider(requested)) throw new Error(`unknown provider "${payload.defaultProvider}"`);
|
|
2044
|
-
nextConfig.defaultProvider = requested;
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
let presets = Array.isArray(nextConfig.presets) ? nextConfig.presets.slice() : [];
|
|
2048
|
-
const workflowRoutes = { ...(nextConfig.workflowRoutes || {}) };
|
|
2049
|
-
// Track slots actually written THIS call so maintenance mirroring never
|
|
2050
|
-
// clobbers an existing slot the payload did not touch.
|
|
2051
|
-
const touchedWorkflowSlots = new Set();
|
|
2052
|
-
|
|
2053
|
-
if (defaultRoute) {
|
|
2054
|
-
presets = upsertWorkflowPreset(presets, 'lead', defaultRoute);
|
|
2055
|
-
workflowRoutes.lead = defaultRoute;
|
|
2056
|
-
nextConfig.default = workflowPresetId('lead');
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
for (const slot of WORKFLOW_ROUTE_SLOTS) {
|
|
2060
|
-
const normalized = normalizeWorkflowRoute(workflowInput[slot]);
|
|
2061
|
-
if (!normalized) continue;
|
|
2062
|
-
workflowRoutes[slot] = normalized;
|
|
2063
|
-
presets = upsertWorkflowPreset(presets, slot, normalized);
|
|
2064
|
-
touchedWorkflowSlots.add(slot);
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
nextConfig.presets = presets;
|
|
2068
|
-
nextConfig.workflowRoutes = workflowRoutes;
|
|
2069
|
-
// Maintenance slots store a direct {provider, model} route. Only mirror a
|
|
2070
|
-
// slot that was actually written this call (touchedWorkflowSlots); an
|
|
2071
|
-
// untouched slot keeps its existing maintenance value.
|
|
2072
|
-
nextConfig.maintenance = {
|
|
2073
|
-
...(nextConfig.maintenance || {}),
|
|
2074
|
-
...(touchedWorkflowSlots.has('explorer') ? { explore: normalizeWorkflowRoute(workflowRoutes.explorer) } : {}),
|
|
2075
|
-
...(touchedWorkflowSlots.has('memory') ? { memory: normalizeWorkflowRoute(workflowRoutes.memory) } : {}),
|
|
2076
|
-
};
|
|
2077
|
-
// Per-agent onboarding routes (id → route) for the full FIXED_AGENT_SLOTS
|
|
2078
|
-
// roster. Mirrors setAgentRoute persistence: config.agents[id], an
|
|
2079
|
-
// agent-<id> preset, and (for workflow-backed agents) the workflowRoute +
|
|
2080
|
-
// maintenance slot. Agents omitted from the payload inherit defaultRoute.
|
|
2081
|
-
const agentInput = payload.agentRoutes && typeof payload.agentRoutes === 'object'
|
|
2082
|
-
? payload.agentRoutes
|
|
2083
|
-
: null;
|
|
2084
|
-
if (agentInput) {
|
|
2085
|
-
const nextAgents = { ...(nextConfig.agents || {}) };
|
|
2086
|
-
const nextMaintenance = { ...(nextConfig.maintenance || {}) };
|
|
2087
|
-
for (const agent of FIXED_AGENT_SLOTS) {
|
|
2088
|
-
// Persist ONLY agents with an explicit override in the payload. An
|
|
2089
|
-
// omitted agent is left untouched (existing value preserved, or
|
|
2090
|
-
// unwritten so the runtime dynamically follows the Main Model). Never
|
|
2091
|
-
// fall back to defaultRoute here — that would overwrite an agent the
|
|
2092
|
-
// user did not choose.
|
|
2093
|
-
const routeToSave = normalizeWorkflowRoute(agentInput[agent.id]);
|
|
2094
|
-
if (!routeToSave) continue;
|
|
2095
|
-
nextAgents[agent.id] = routeToSave;
|
|
2096
|
-
presets = upsertWorkflowPreset(presets, agentPresetSlot(agent.id), routeToSave);
|
|
2097
|
-
if (agent.workflowSlot) {
|
|
2098
|
-
workflowRoutes[agent.workflowSlot] = routeToSave;
|
|
2099
|
-
presets = upsertWorkflowPreset(presets, agent.workflowSlot, routeToSave);
|
|
2100
|
-
if (agent.id === 'explore') nextMaintenance.explore = routeToSave;
|
|
2101
|
-
if (agent.id === 'maintainer') nextMaintenance.memory = routeToSave;
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
nextConfig.agents = nextAgents;
|
|
2105
|
-
nextConfig.presets = presets;
|
|
2106
|
-
nextConfig.workflowRoutes = workflowRoutes;
|
|
2107
|
-
nextConfig.maintenance = nextMaintenance;
|
|
2108
|
-
}
|
|
2109
|
-
nextConfig.onboarding = {
|
|
2110
|
-
...(nextConfig.onboarding || {}),
|
|
2111
|
-
completed: true,
|
|
2112
|
-
version: ONBOARDING_VERSION,
|
|
2113
|
-
completedAt: new Date().toISOString(),
|
|
2114
|
-
};
|
|
2115
|
-
|
|
2116
|
-
// Optional native web-search route. Only persisted when provided; mirrors
|
|
2117
|
-
// setSearchRoute's config write (skip provider capability validation here
|
|
2118
|
-
// since onboarding routes come from the vetted provider model list).
|
|
2119
|
-
if (payload.searchRoute) {
|
|
2120
|
-
const searchToSave = normalizeSearchRouteConfig(payload.searchRoute);
|
|
2121
|
-
if (searchToSave) nextConfig.searchRoute = searchToSave;
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
saveConfigAndAdopt(nextConfig);
|
|
2125
|
-
if (defaultRoute) {
|
|
2126
|
-
route = resolveRoute(config, { provider: defaultRoute.provider, model: defaultRoute.model, effort: defaultRoute.effort });
|
|
2127
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-onboarding-complete');
|
|
2128
|
-
await recreateCurrentSessionIfReady();
|
|
2129
|
-
}
|
|
2130
|
-
return this.getOnboardingStatus();
|
|
2131
|
-
},
|
|
2132
|
-
getChannelSetup() {
|
|
2133
|
-
// Flush a pending debounced backend switch first so setup readers
|
|
2134
|
-
// (Settings → Channel Setting, remote toggles) never observe the
|
|
2135
|
-
// previous backend during the 150ms debounce window.
|
|
2136
|
-
try { flushBackendSave(); } catch {}
|
|
2137
|
-
return channelSetup();
|
|
2138
|
-
},
|
|
2139
|
-
getChannelWorkerStatus() {
|
|
2140
|
-
return channels.status();
|
|
2141
|
-
},
|
|
2142
|
-
startRemote() {
|
|
2143
|
-
return startRemote();
|
|
2144
|
-
},
|
|
2145
|
-
stopRemote(reason) {
|
|
2146
|
-
return stopRemote(reason);
|
|
2147
|
-
},
|
|
2148
|
-
isRemoteEnabled() {
|
|
2149
|
-
return isRemoteEnabled();
|
|
2150
|
-
},
|
|
2151
|
-
// Subscribe to non-user-initiated remote flips (seat superseded). Returns
|
|
2152
|
-
// an unsubscribe function. TUI uses this to sync its Remote indicator and
|
|
2153
|
-
// show a "remote taken over" notice.
|
|
2154
|
-
onRemoteStateChange(listener) {
|
|
2155
|
-
if (typeof listener !== 'function') return () => {};
|
|
2156
|
-
remoteStateListeners.add(listener);
|
|
2157
|
-
return () => remoteStateListeners.delete(listener);
|
|
2158
|
-
},
|
|
2159
|
-
setChannel(entry) {
|
|
2160
|
-
const result = setChannel(entry);
|
|
2161
|
-
reloadChannelsSoon();
|
|
2162
|
-
return result;
|
|
2163
|
-
},
|
|
2164
|
-
setWebhookConfig(patch) {
|
|
2165
|
-
const result = setWebhookConfig(patch);
|
|
2166
|
-
reloadChannelsSoon();
|
|
2167
|
-
return result;
|
|
2168
|
-
},
|
|
2169
|
-
saveSchedule(entry) {
|
|
2170
|
-
const result = saveSchedule(entry);
|
|
2171
|
-
reloadChannelsSoon();
|
|
2172
|
-
return result;
|
|
2173
|
-
},
|
|
2174
|
-
deleteSchedule(name) {
|
|
2175
|
-
const result = deleteSchedule(name);
|
|
2176
|
-
reloadChannelsSoon();
|
|
2177
|
-
return result;
|
|
2178
|
-
},
|
|
2179
|
-
setScheduleEnabled(name, enabled) {
|
|
2180
|
-
const result = setScheduleEnabled(name, enabled);
|
|
2181
|
-
reloadChannelsSoon();
|
|
2182
|
-
return result;
|
|
2183
|
-
},
|
|
2184
|
-
saveWebhook(entry) {
|
|
2185
|
-
const result = saveWebhook(entry);
|
|
2186
|
-
reloadChannelsSoon();
|
|
2187
|
-
return result;
|
|
2188
|
-
},
|
|
2189
|
-
deleteWebhook(name) {
|
|
2190
|
-
const result = deleteWebhook(name);
|
|
2191
|
-
reloadChannelsSoon();
|
|
2192
|
-
return result;
|
|
2193
|
-
},
|
|
2194
|
-
setWebhookEnabled(name, enabled) {
|
|
2195
|
-
const result = setWebhookEnabled(name, enabled);
|
|
2196
|
-
reloadChannelsSoon();
|
|
2197
|
-
return result;
|
|
2198
|
-
},
|
|
2199
|
-
async authenticateProvider(providerId, secret) {
|
|
2200
|
-
const result = String(secret || '').trim()
|
|
2201
|
-
? saveProviderApiKey(cfgMod, providerId, secret)
|
|
2202
|
-
: await loginOAuthProvider(cfgMod, providerId);
|
|
2203
|
-
reloadFullConfig();
|
|
2204
|
-
invalidateProviderCaches();
|
|
2205
|
-
warmProviderModelCache();
|
|
2206
|
-
return result;
|
|
2207
|
-
},
|
|
2208
|
-
async loginOAuthProvider(providerId) {
|
|
2209
|
-
const result = await loginOAuthProvider(cfgMod, providerId);
|
|
2210
|
-
reloadFullConfig();
|
|
2211
|
-
invalidateProviderCaches();
|
|
2212
|
-
warmProviderModelCache();
|
|
2213
|
-
return result;
|
|
2214
|
-
},
|
|
2215
|
-
async beginOAuthProviderLogin(providerId) {
|
|
2216
|
-
const result = await beginOAuthProviderLogin(cfgMod, providerId);
|
|
2217
|
-
reloadFullConfig();
|
|
2218
|
-
return {
|
|
2219
|
-
...result,
|
|
2220
|
-
waitForCallback: result.waitForCallback?.then((completed) => {
|
|
2221
|
-
reloadFullConfig();
|
|
2222
|
-
if (completed) {
|
|
2223
|
-
invalidateProviderCaches();
|
|
2224
|
-
warmProviderModelCache();
|
|
2225
|
-
}
|
|
2226
|
-
return completed;
|
|
2227
|
-
}),
|
|
2228
|
-
completeCode: async (code) => {
|
|
2229
|
-
const completed = await result.completeCode(code);
|
|
2230
|
-
reloadFullConfig();
|
|
2231
|
-
invalidateProviderCaches();
|
|
2232
|
-
warmProviderModelCache();
|
|
2233
|
-
return completed;
|
|
2234
|
-
},
|
|
2235
|
-
};
|
|
2236
|
-
},
|
|
2237
|
-
saveProviderApiKey(providerId, secret) {
|
|
2238
|
-
const result = saveProviderApiKey(cfgMod, providerId, secret);
|
|
2239
|
-
reloadFullConfig();
|
|
2240
|
-
invalidateProviderCaches();
|
|
2241
|
-
warmProviderModelCache();
|
|
2242
|
-
return result;
|
|
2243
|
-
},
|
|
2244
|
-
saveOpenAIUsageSessionKey(secret) {
|
|
2245
|
-
const result = saveOpenAIUsageSessionKey(cfgMod, secret);
|
|
2246
|
-
reloadFullConfig();
|
|
2247
|
-
invalidateProviderCaches();
|
|
2248
|
-
return result;
|
|
2249
|
-
},
|
|
2250
|
-
saveOpenCodeGoUsageAuth(opts) {
|
|
2251
|
-
const result = saveOpenCodeGoUsageAuth(cfgMod, opts);
|
|
2252
|
-
reloadFullConfig();
|
|
2253
|
-
invalidateProviderCaches();
|
|
2254
|
-
return result;
|
|
2255
|
-
},
|
|
2256
|
-
async loginOpenCodeGoUsage() {
|
|
2257
|
-
const result = await loginOpenCodeGoUsage(cfgMod);
|
|
2258
|
-
reloadFullConfig();
|
|
2259
|
-
invalidateProviderCaches();
|
|
2260
|
-
return result;
|
|
2261
|
-
},
|
|
2262
|
-
setLocalProvider(providerId, opts) {
|
|
2263
|
-
const result = setLocalProvider(cfgMod, providerId, opts);
|
|
2264
|
-
reloadFullConfig();
|
|
2265
|
-
invalidateProviderCaches();
|
|
2266
|
-
warmProviderModelCache();
|
|
2267
|
-
return result;
|
|
2268
|
-
},
|
|
2269
|
-
forgetProviderAuth(providerId) {
|
|
2270
|
-
const result = forgetProviderAuth(cfgMod, providerId);
|
|
2271
|
-
reloadFullConfig();
|
|
2272
|
-
invalidateProviderCaches();
|
|
2273
|
-
warmProviderModelCache();
|
|
2274
|
-
return result;
|
|
2275
|
-
},
|
|
2276
|
-
listPresets() {
|
|
2277
|
-
return cfgMod.listPresets(displayConfig());
|
|
2278
|
-
},
|
|
2279
|
-
async listProviderModels(options = {}) {
|
|
2280
|
-
return await collectProviderModels({
|
|
2281
|
-
force: options.force === true || options.refresh === true,
|
|
2282
|
-
quick: options.quick === true,
|
|
2283
|
-
});
|
|
2284
|
-
},
|
|
2285
|
-
getSearchRoute() {
|
|
2286
|
-
searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
|
|
2287
|
-
return searchRoute;
|
|
2288
|
-
},
|
|
2289
|
-
async listSearchModels(options = {}) {
|
|
2290
|
-
return await collectSearchProviderModels({ force: options.force === true || options.refresh === true });
|
|
2291
|
-
},
|
|
2292
|
-
async setSearchRoute(next) {
|
|
2293
|
-
let selectedRoute = normalizeSearchRouteConfig(next);
|
|
2294
|
-
if (!selectedRoute && next?.model && searchRoute?.provider) {
|
|
2295
|
-
selectedRoute = normalizeSearchRouteConfig({ ...next, provider: searchRoute.provider });
|
|
2296
|
-
}
|
|
2297
|
-
if (!selectedRoute) throw new Error('search route requires provider and model');
|
|
2298
|
-
if (isDefaultSearchRouteConfig(selectedRoute)) {
|
|
2299
|
-
ensureFullConfig();
|
|
2300
|
-
const routeToSave = normalizeSearchRouteConfig({
|
|
2301
|
-
provider: SEARCH_DEFAULT_PROVIDER,
|
|
2302
|
-
model: SEARCH_DEFAULT_MODEL,
|
|
2303
|
-
...(selectedRoute.toolType ? { toolType: selectedRoute.toolType } : {}),
|
|
2304
|
-
});
|
|
2305
|
-
const nextConfig = { ...config };
|
|
2306
|
-
nextConfig.searchRoute = routeToSave;
|
|
2307
|
-
saveConfigAndAdopt(nextConfig);
|
|
2308
|
-
searchRoute = normalizeSearchRouteConfig(config.searchRoute);
|
|
2309
|
-
invalidateProviderCaches();
|
|
2310
|
-
return searchRoute;
|
|
2311
|
-
}
|
|
2312
|
-
if (!isSearchCapableProvider(selectedRoute.provider)) {
|
|
2313
|
-
throw new Error(`provider "${selectedRoute.provider}" does not support Mixdog native search`);
|
|
2314
|
-
}
|
|
2315
|
-
ensureFullConfig();
|
|
2316
|
-
await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
|
|
2317
|
-
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
2318
|
-
if (!searchCapableFor(selectedRoute.provider, modelMeta)) {
|
|
2319
|
-
throw new Error(`model "${selectedRoute.model}" is not marked as web-search capable`);
|
|
2320
|
-
}
|
|
2321
|
-
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
2322
|
-
const effort = coerceEffortFor(selectedRoute.provider, modelMeta, selectedRoute.effort);
|
|
2323
|
-
selectedRoute = {
|
|
2324
|
-
...selectedRoute,
|
|
2325
|
-
...(effort ? { effort } : {}),
|
|
2326
|
-
fast: fastCapable ? selectedRoute.fast === true : false,
|
|
2327
|
-
};
|
|
2328
|
-
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
2329
|
-
const routeToSave = normalizeSearchRouteConfig(selectedRoute);
|
|
2330
|
-
const nextConfig = { ...config };
|
|
2331
|
-
nextConfig.searchRoute = routeToSave;
|
|
2332
|
-
saveConfigAndAdopt(nextConfig);
|
|
2333
|
-
searchRoute = normalizeSearchRouteConfig(config.searchRoute);
|
|
2334
|
-
invalidateProviderCaches();
|
|
2335
|
-
return searchRoute;
|
|
2336
|
-
},
|
|
2337
|
-
listAgents() {
|
|
2338
|
-
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
2339
|
-
return FIXED_AGENT_SLOTS.map((agent) => ({
|
|
2340
|
-
...agent,
|
|
2341
|
-
locked: true,
|
|
2342
|
-
route: agentRouteFromConfig(config, agent.id, dataDir),
|
|
2343
|
-
definition: loadAgentDefinition(dataDir, agent.id),
|
|
2344
|
-
}));
|
|
2345
|
-
},
|
|
2346
|
-
listWorkflows() {
|
|
2347
|
-
const currentConfig = displayConfig();
|
|
2348
|
-
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
2349
|
-
const active = activeWorkflowId(currentConfig);
|
|
2350
|
-
return listWorkflowPacks(dataDir).map((workflow) => ({
|
|
2351
|
-
id: workflow.id,
|
|
2352
|
-
name: workflow.name,
|
|
2353
|
-
description: workflow.description,
|
|
2354
|
-
source: workflow.source,
|
|
2355
|
-
active: workflow.id === active,
|
|
2356
|
-
agents: workflow.agents,
|
|
2357
|
-
}));
|
|
2358
|
-
},
|
|
2359
|
-
getOutputStyle() {
|
|
2360
|
-
return getOutputStyleStatusCached();
|
|
2361
|
-
},
|
|
2362
|
-
listOutputStyles() {
|
|
2363
|
-
return getOutputStyleStatusCached();
|
|
2364
|
-
},
|
|
2365
|
-
async setOutputStyle(value) {
|
|
2366
|
-
const before = getOutputStyleStatusCached({ fresh: true });
|
|
2367
|
-
const selected = findOutputStyle(value, before.styles);
|
|
2368
|
-
if (!selected) {
|
|
2369
|
-
const names = before.styles.map((style) => style.label || style.id).join(', ') || 'Default';
|
|
2370
|
-
throw new Error(`output style must be one of ${names}`);
|
|
2371
|
-
}
|
|
2372
|
-
// Adopt in-memory immediately so same-tick readers see the new style;
|
|
2373
|
-
// persist off the key-handler tick. NOTE: the disk write goes through
|
|
2374
|
-
// the dedicated flushOutputStyleSave debounce (sharedCfgMod whole-root
|
|
2375
|
-
// RMW) — cfgMod.saveConfig only serializes agent-section fields and
|
|
2376
|
-
// would drop the top-level outputStyle key.
|
|
2377
|
-
const nextConfig = { ...config, outputStyle: selected.id };
|
|
2378
|
-
if (nextConfig.agent && typeof nextConfig.agent === 'object' && !Array.isArray(nextConfig.agent)) {
|
|
2379
|
-
const agent = { ...nextConfig.agent };
|
|
2380
|
-
delete agent.outputStyle;
|
|
2381
|
-
nextConfig.agent = agent;
|
|
2382
|
-
}
|
|
2383
|
-
adoptConfig(nextConfig);
|
|
2384
|
-
scheduleOutputStyleSave(selected.id);
|
|
2385
|
-
// Reuse the catalog already scanned above for `before` instead of a
|
|
2386
|
-
// second forced-fresh filesystem scan; refresh the status cache
|
|
2387
|
-
// in-memory (short TTL, same as getOutputStyleStatusCached) so reads
|
|
2388
|
-
// during the debounce window see the just-selected style.
|
|
2389
|
-
const freshStatus = { configured: selected.id, current: selected, styles: before.styles };
|
|
2390
|
-
seedOutputStyleStatusCache(freshStatus);
|
|
2391
|
-
const hasConversation = sessionHasConversationMessages(session);
|
|
2392
|
-
let appliedToCurrentSession = !hasConversation;
|
|
2393
|
-
if (session?.id && !hasConversation) {
|
|
2394
|
-
const closedSessionId = session.id;
|
|
2395
|
-
mgr.closeSession(closedSessionId, 'cli-output-style-switch');
|
|
2396
|
-
session = null;
|
|
2397
|
-
// Defer the recreate so the output-style picker can repaint first;
|
|
2398
|
-
// any failure still surfaces via the existing notice path.
|
|
2399
|
-
setTimeout(() => {
|
|
2400
|
-
recreateCurrentSessionIfReady().catch((err) => {
|
|
2401
|
-
try {
|
|
2402
|
-
notifyFnForSession(closedSessionId)(
|
|
2403
|
-
`Failed to start a new session after output style change: ${err?.message || err}`,
|
|
2404
|
-
{ level: 'error' },
|
|
2405
|
-
);
|
|
2406
|
-
} catch {}
|
|
2407
|
-
});
|
|
2408
|
-
}, 0);
|
|
2409
|
-
}
|
|
2410
|
-
invalidateContextStatusCache();
|
|
2411
|
-
return { ...freshStatus, appliedToCurrentSession };
|
|
2412
|
-
},
|
|
2413
|
-
async setWorkflow(workflowId) {
|
|
2414
|
-
const id = normalizeWorkflowId(workflowId, DEFAULT_WORKFLOW_ID);
|
|
2415
|
-
const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
|
|
2416
|
-
const pack = loadWorkflowPack(dataDir, id);
|
|
2417
|
-
if (!pack || pack.id !== id) throw new Error(`workflow "${workflowId}" not found`);
|
|
2418
|
-
const nextConfig = { ...config };
|
|
2419
|
-
nextConfig.workflow = { ...(nextConfig.workflow || {}), active: id };
|
|
2420
|
-
saveConfigAndAdopt(nextConfig);
|
|
2421
|
-
return workflowSummary(pack);
|
|
2422
|
-
},
|
|
2423
|
-
async setAgentRoute(agentId, next) {
|
|
2424
|
-
const id = normalizeAgentId(agentId);
|
|
2425
|
-
if (!id) throw new Error(`unknown agent "${agentId}"`);
|
|
2426
|
-
let selectedRoute = resolveRoute(config, { ...(next || {}) });
|
|
2427
|
-
await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
|
|
2428
|
-
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
2429
|
-
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
2430
|
-
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
2431
|
-
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
2432
|
-
|
|
2433
|
-
const routeToSave = normalizeWorkflowRoute(selectedRoute);
|
|
2434
|
-
if (!routeToSave) throw new Error('agent route requires provider and model');
|
|
2435
|
-
const agent = FIXED_AGENT_SLOTS.find((item) => item.id === id);
|
|
2436
|
-
const nextConfig = { ...config };
|
|
2437
|
-
nextConfig.agents = {
|
|
2438
|
-
...(nextConfig.agents || {}),
|
|
2439
|
-
[id]: routeToSave,
|
|
2440
|
-
};
|
|
2441
|
-
nextConfig.presets = upsertWorkflowPreset(nextConfig.presets, agentPresetSlot(id), routeToSave);
|
|
2442
|
-
if (agent?.workflowSlot) {
|
|
2443
|
-
nextConfig.workflowRoutes = {
|
|
2444
|
-
...(nextConfig.workflowRoutes || {}),
|
|
2445
|
-
[agent.workflowSlot]: routeToSave,
|
|
2446
|
-
};
|
|
2447
|
-
nextConfig.presets = upsertWorkflowPreset(nextConfig.presets, agent.workflowSlot, routeToSave);
|
|
2448
|
-
// Maintenance slots store a direct {provider, model} route now, so
|
|
2449
|
-
// mirror the agent route straight in instead of a preset-id string.
|
|
2450
|
-
nextConfig.maintenance = {
|
|
2451
|
-
...(nextConfig.maintenance || {}),
|
|
2452
|
-
...(id === 'explore' ? { explore: routeToSave } : {}),
|
|
2453
|
-
...(id === 'maintainer' ? { memory: routeToSave } : {}),
|
|
2454
|
-
};
|
|
2455
|
-
}
|
|
2456
|
-
saveConfigAndAdopt(nextConfig);
|
|
2457
|
-
return routeToSave;
|
|
2458
|
-
},
|
|
2459
|
-
async setDefaultProvider(provider) {
|
|
2460
|
-
const requested = clean(provider);
|
|
2461
|
-
if (!requested) throw new Error('provider is required');
|
|
2462
|
-
if (!isKnownProvider(requested)) throw new Error(`unknown provider "${provider}"`);
|
|
2463
|
-
saveConfigAndAdopt({ ...config, defaultProvider: requested });
|
|
2464
|
-
return requested;
|
|
2465
|
-
},
|
|
2466
|
-
async ask(prompt, options = {}) {
|
|
2467
|
-
activeTurnCount += 1;
|
|
2468
|
-
// Lazy code-graph prewarm: kick off the build ONCE, on the first real
|
|
2469
|
-
// turn, so a likely code lookup hits a warm cache — without paying the
|
|
2470
|
-
// post-first-frame prewarm freeze on idle startup. The schedule is async
|
|
2471
|
-
// + unref'd (worker thread), so it never blocks this turn.
|
|
2472
|
-
if (codeGraphPrewarmLazy && !codeGraphFirstTurnPrewarmDone) {
|
|
2473
|
-
codeGraphFirstTurnPrewarmDone = true;
|
|
2474
|
-
scheduleCodeGraphPrewarm(0, 'first-turn');
|
|
2475
|
-
}
|
|
2476
|
-
const startedAt = Date.now();
|
|
2477
|
-
try {
|
|
2478
|
-
await refreshSessionForCwdIfNeeded('cwd-change');
|
|
2479
|
-
if (!session?.id) await createCurrentSession('turn');
|
|
2480
|
-
// Remote outbound: ensure a transcript writer bound to the current
|
|
2481
|
-
// session.id + cwd. The channel worker's OutputForwarder tails this
|
|
2482
|
-
// JSONL and pushes the surface view to Discord. Gated on remoteEnabled
|
|
2483
|
-
// so non-remote sessions write nothing.
|
|
2484
|
-
if (remoteEnabled) {
|
|
2485
|
-
// Reset per-turn dedup tracker so an identical answer in a later turn
|
|
2486
|
-
// is still written (the guard only prevents same-turn double-write).
|
|
2487
|
-
_lastAppendedAssistant = '';
|
|
2488
|
-
const prevKey = _twKey;
|
|
2489
|
-
ensureRemoteTranscriptWriter();
|
|
2490
|
-
// The writer moved to a NEW session/cwd (e.g. lazy first-turn
|
|
2491
|
-
// session create after /remote, or /new mid-remote). Re-pin the
|
|
2492
|
-
// channel worker's output forwarder onto the fresh transcript NOW —
|
|
2493
|
-
// otherwise it keeps tailing the previous binding until an inbound
|
|
2494
|
-
// Discord message triggers the steal path, so terminal-initiated
|
|
2495
|
-
// turns would never forward. activate_channel_bridge is idempotent
|
|
2496
|
-
// force-takeover (claim + rebind); fire-and-forget so the turn is
|
|
2497
|
-
// never blocked on worker IPC.
|
|
2498
|
-
if (_twKey && _twKey !== prevKey && channelsEnabled() && !envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
2499
|
-
void invokeChannelStart()
|
|
2500
|
-
.then(() => {
|
|
2501
|
-
// stopRemote()/superseded/close may have landed while the
|
|
2502
|
-
// worker was starting — never re-claim for a turned-off session.
|
|
2503
|
-
if (!remoteEnabled || closeRequested) return undefined;
|
|
2504
|
-
return channels.execute('activate_channel_bridge', { active: true });
|
|
2505
|
-
})
|
|
2506
|
-
.catch((error) => bootProfile('channels:turn-rebind-failed', { error: error?.message || String(error) }));
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
hooks.emit('turn:start', { sessionId: session.id, prompt, cwd: currentCwd });
|
|
2510
|
-
// UserPromptSubmit: bridge to the standard hook bus. A hook FAILURE
|
|
2511
|
-
// must not block the turn, but a genuine blocked===true MUST throw.
|
|
2512
|
-
let promptDispatch = null;
|
|
2513
|
-
try {
|
|
2514
|
-
promptDispatch = await hooks.dispatch('UserPromptSubmit', hookCommonPayload({ session_id: session.id, prompt }));
|
|
2515
|
-
} catch { /* hook failure never blocks the turn */ }
|
|
2516
|
-
if (promptDispatch?.blocked === true) {
|
|
2517
|
-
throw new Error(`prompt blocked by hook: ${promptDispatch.reason || ''}`);
|
|
2518
|
-
}
|
|
2519
|
-
const hookContext = Array.isArray(promptDispatch?.additionalContext)
|
|
2520
|
-
? promptDispatch.additionalContext.join('\n\n')
|
|
2521
|
-
: String(promptDispatch?.additionalContext || '');
|
|
2522
|
-
const turnContext = [options.context || '', hookContext]
|
|
2523
|
-
.map((part) => String(part || '').trim())
|
|
2524
|
-
.filter(Boolean)
|
|
2525
|
-
.join('\n\n');
|
|
2526
|
-
const result = await mgr.askSession(
|
|
2527
|
-
session.id,
|
|
2528
|
-
prompt,
|
|
2529
|
-
turnContext || null,
|
|
2530
|
-
async (iter, calls) => {
|
|
2531
|
-
for (const call of calls || []) {
|
|
2532
|
-
hooks.emit('tool:planned', {
|
|
2533
|
-
sessionId: session.id,
|
|
2534
|
-
name: call?.name || 'tool',
|
|
2535
|
-
callId: call?.id || null,
|
|
2536
|
-
});
|
|
2537
|
-
// Mirror each tool card the TUI shows into the transcript so the
|
|
2538
|
-
// forwarder renders the same one-line tool summary on Discord.
|
|
2539
|
-
// calls carry { name, arguments, id } (loop.mjs response.toolCalls);
|
|
2540
|
-
// some providers use `input` — accept either.
|
|
2541
|
-
if (remoteEnabled && _transcriptWriter) {
|
|
2542
|
-
try { _transcriptWriter.appendToolUse(call?.name, call?.input ?? call?.arguments); }
|
|
2543
|
-
catch (error) { process.stderr.write(`mixdog: transcript-writer: onToolCall failed: ${error?.message || error}\n`); }
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
if (typeof options.onToolCall === 'function') {
|
|
2547
|
-
return await options.onToolCall(iter, calls);
|
|
2548
|
-
}
|
|
2549
|
-
return undefined;
|
|
2550
|
-
},
|
|
2551
|
-
currentCwd,
|
|
2552
|
-
options.prefetch || null,
|
|
2553
|
-
{
|
|
2554
|
-
onTextDelta: options.onTextDelta,
|
|
2555
|
-
onReasoningDelta: options.onReasoningDelta,
|
|
2556
|
-
onAssistantText: (text) => {
|
|
2557
|
-
// Capture the same assistant text the TUI renders (final answer +
|
|
2558
|
-
// mid-turn preamble). Feed the writer, then ALWAYS call through to
|
|
2559
|
-
// the caller's hook so terminal rendering is never affected.
|
|
2560
|
-
if (remoteEnabled && _transcriptWriter) {
|
|
2561
|
-
try {
|
|
2562
|
-
const value = typeof text === 'string' ? text : (text == null ? '' : String(text));
|
|
2563
|
-
if (value.trim()) {
|
|
2564
|
-
_transcriptWriter.appendAssistant(value);
|
|
2565
|
-
// Track the last line we wrote so the post-turn final
|
|
2566
|
-
// append below can skip an identical re-write (buffered
|
|
2567
|
-
// providers surface the final answer here AND in result).
|
|
2568
|
-
_lastAppendedAssistant = value;
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
catch (error) { process.stderr.write(`mixdog: transcript-writer: onAssistantText failed: ${error?.message || error}\n`); }
|
|
2572
|
-
}
|
|
2573
|
-
return options.onAssistantText?.(text);
|
|
2574
|
-
},
|
|
2575
|
-
onUsageDelta: options.onUsageDelta,
|
|
2576
|
-
onToolResult: (message) => {
|
|
2577
|
-
// Surface Edit diffs only: the forwarder reads toolUseResult
|
|
2578
|
-
// {oldString,newString}. Other tool results carry no diff payload,
|
|
2579
|
-
// so we append only when those fields are present.
|
|
2580
|
-
if (remoteEnabled && _transcriptWriter) {
|
|
2581
|
-
try {
|
|
2582
|
-
const tur = message?.toolUseResult;
|
|
2583
|
-
if (tur && (tur.oldString != null || tur.newString != null)) {
|
|
2584
|
-
_transcriptWriter.appendToolResult({ oldString: tur.oldString ?? '', newString: tur.newString ?? '' });
|
|
2585
|
-
}
|
|
2586
|
-
} catch (error) { process.stderr.write(`mixdog: transcript-writer: onToolResult failed: ${error?.message || error}\n`); }
|
|
2587
|
-
}
|
|
2588
|
-
return options.onToolResult?.(message);
|
|
2589
|
-
},
|
|
2590
|
-
onToolApproval: options.onToolApproval,
|
|
2591
|
-
onCompactEvent: options.onCompactEvent,
|
|
2592
|
-
onStageChange: options.onStageChange,
|
|
2593
|
-
onStreamDelta: options.onStreamDelta,
|
|
2594
|
-
drainSteering: options.drainSteering,
|
|
2595
|
-
onSteerMessage: options.onSteerMessage,
|
|
2596
|
-
notifyFn: notifyFnForSession(session.id),
|
|
2597
|
-
},
|
|
2598
|
-
);
|
|
2599
|
-
session = mgr.getSession(session.id) || session;
|
|
2600
|
-
// Final assistant text: onAssistantText fires only for buffered
|
|
2601
|
-
// (non-streaming) providers and for mid-turn preamble. Streaming
|
|
2602
|
-
// providers deliver the final answer via onTextDelta (which we do NOT
|
|
2603
|
-
// tap, to avoid per-delta spam), so append the authoritative final
|
|
2604
|
-
// content here from result.content. To avoid double-writing the
|
|
2605
|
-
// buffered case, only append when it differs from the last assistant
|
|
2606
|
-
// line onAssistantText already wrote.
|
|
2607
|
-
if (remoteEnabled && _transcriptWriter) {
|
|
2608
|
-
try {
|
|
2609
|
-
const finalText = result?.content != null ? String(result.content) : '';
|
|
2610
|
-
if (finalText.trim() && finalText !== _lastAppendedAssistant) {
|
|
2611
|
-
_transcriptWriter.appendAssistant(finalText);
|
|
2612
|
-
_lastAppendedAssistant = finalText;
|
|
2613
|
-
}
|
|
2614
|
-
} catch (error) {
|
|
2615
|
-
process.stderr.write(`mixdog: transcript-writer: final append failed: ${error?.message || error}\n`);
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
hooks.emit('turn:end', { sessionId: session.id, elapsedMs: Date.now() - startedAt });
|
|
2619
|
-
// Stop: bridge to the standard hook bus. Best-effort; ignore result,
|
|
2620
|
-
// never throw.
|
|
2621
|
-
try {
|
|
2622
|
-
await hooks.dispatch('Stop', hookCommonPayload({ session_id: session.id }));
|
|
2623
|
-
} catch { /* best-effort: Stop hook must never break the turn */ }
|
|
2624
|
-
return { result, session };
|
|
2625
|
-
} catch (error) {
|
|
2626
|
-
hooks.emit('turn:error', { sessionId: session?.id || null, elapsedMs: Date.now() - startedAt, error: error?.message || String(error) });
|
|
2627
|
-
// StopFailure: bridge a turn error to the standard hook bus. Spec:
|
|
2628
|
-
// output + exit code ignored, so pure fire-and-forget. error_type is a
|
|
2629
|
-
// simple regex mapping from the error message (default 'unknown').
|
|
2630
|
-
try {
|
|
2631
|
-
const msg = String(error?.message || error || '').toLowerCase();
|
|
2632
|
-
const errorType = /rate.?limit|429|too many requests/.test(msg) ? 'rate_limit'
|
|
2633
|
-
: /overloaded|529/.test(msg) ? 'overloaded'
|
|
2634
|
-
: /authenticat|unauthorized|401|invalid.*api.?key/.test(msg) ? 'authentication_failed'
|
|
2635
|
-
: /server.?error|5\d\d|internal error/.test(msg) ? 'server_error'
|
|
2636
|
-
: 'unknown';
|
|
2637
|
-
void hooks.dispatch('StopFailure', hookCommonPayload({ session_id: session?.id || null, error_type: errorType }));
|
|
2638
|
-
} catch { /* best-effort: StopFailure hook must never break teardown */ }
|
|
2639
|
-
throw error;
|
|
2640
|
-
} finally {
|
|
2641
|
-
activeTurnCount = Math.max(0, activeTurnCount - 1);
|
|
2642
|
-
if (!firstTurnCompleted) {
|
|
2643
|
-
firstTurnCompleted = true;
|
|
2644
|
-
scheduleProviderWarmup();
|
|
2645
|
-
scheduleProviderModelWarmup();
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
},
|
|
2649
|
-
async clear(options = {}) {
|
|
2650
|
-
if (!session?.id) return false;
|
|
2651
|
-
const cleared = await mgr.clearSessionMessages(session.id, options);
|
|
2652
|
-
if (!cleared) return false;
|
|
2653
|
-
session = typeof cleared === 'object' ? cleared : (mgr.getSession(session.id) || session);
|
|
2654
|
-
if (options.recoverAgent === true) {
|
|
2655
|
-
try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
|
|
2656
|
-
}
|
|
2657
|
-
invalidateContextStatusCache();
|
|
2658
|
-
return true;
|
|
2659
|
-
},
|
|
2660
|
-
// session_manage tool handoff: the engine polls this at turn end and, if
|
|
2661
|
-
// set, runs the same clear path the idle auto-clear uses. One-shot read.
|
|
2662
|
-
consumePendingSessionReset() {
|
|
2663
|
-
const pending = pendingSessionReset;
|
|
2664
|
-
pendingSessionReset = null;
|
|
2665
|
-
if (!pending) return null;
|
|
2666
|
-
// Session changed since scheduling (resume / new session) — drop it.
|
|
2667
|
-
if (!session?.id || pending.sessionId !== session.id) return null;
|
|
2668
|
-
return pending.action;
|
|
2669
|
-
},
|
|
2670
|
-
async compact(options = {}) {
|
|
2671
|
-
if (!session?.id) return null;
|
|
2672
|
-
if (activeTurnCount > 0) {
|
|
2673
|
-
return { changed: false, reason: 'compact skipped: turn in progress' };
|
|
2674
|
-
}
|
|
2675
|
-
// Manual compact bypasses loop.mjs, so its PreCompact/PostCompact never
|
|
2676
|
-
// fire here — dispatch them explicitly via the session-property hooks
|
|
2677
|
-
// (wired at session create). Best-effort; a hook must not block compaction.
|
|
2678
|
-
try { await session.preCompactHook?.({ trigger: 'manual' }); }
|
|
2679
|
-
catch { /* best-effort: PreCompact hook must never break manual compact */ }
|
|
2680
|
-
const result = await mgr.compactSessionMessages(session.id);
|
|
2681
|
-
try { await session.postCompactHook?.({ trigger: 'manual' }); }
|
|
2682
|
-
catch { /* best-effort: PostCompact hook must never break manual compact */ }
|
|
2683
|
-
session = mgr.getSession(session.id) || session;
|
|
2684
|
-
if (options.recoverAgent === true) {
|
|
2685
|
-
try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
|
|
2686
|
-
}
|
|
2687
|
-
invalidateContextStatusCache();
|
|
2688
|
-
return result;
|
|
2689
|
-
},
|
|
2690
|
-
async setToolMode(nextMode) {
|
|
2691
|
-
mode = normalizeToolMode(nextMode);
|
|
2692
|
-
invalidatePreSessionToolSurface();
|
|
2693
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-mode-switch');
|
|
2694
|
-
await recreateCurrentSessionIfReady();
|
|
2695
|
-
return mode;
|
|
2696
|
-
},
|
|
2697
|
-
agentStatus() {
|
|
2698
|
-
return agentStatusState();
|
|
2699
|
-
},
|
|
2700
|
-
get clientHostPid() {
|
|
2701
|
-
return session?.clientHostPid || process.pid;
|
|
2702
|
-
},
|
|
2703
|
-
agentControl(args = {}) {
|
|
2704
|
-
const callerSessionId = session?.id || null;
|
|
2705
|
-
return agentTool.execute(args, {
|
|
2706
|
-
callerCwd: currentCwd,
|
|
2707
|
-
invocationSource: 'user-command',
|
|
2708
|
-
callerSessionId,
|
|
2709
|
-
clientHostPid: session?.clientHostPid || process.pid,
|
|
2710
|
-
notifyFn: notifyFnForSession(callerSessionId),
|
|
2711
|
-
});
|
|
2712
|
-
},
|
|
2713
|
-
onNotification(listener) {
|
|
2714
|
-
if (typeof listener !== 'function') return () => {};
|
|
2715
|
-
notificationListeners.add(listener);
|
|
2716
|
-
return () => notificationListeners.delete(listener);
|
|
2717
|
-
},
|
|
2718
|
-
toolsStatus(query = '') {
|
|
2719
|
-
const surface = activeToolSurface();
|
|
2720
|
-
const catalog = Array.isArray(surface?.deferredToolCatalog)
|
|
2721
|
-
? surface.deferredToolCatalog
|
|
2722
|
-
: (Array.isArray(surface?.tools) ? surface.tools : []);
|
|
2723
|
-
const activeNames = new Set((surface?.tools || []).map((tool) => tool?.name).filter(Boolean));
|
|
2724
|
-
const needle = clean(query).toLowerCase();
|
|
2725
|
-
const rows = catalog.map((tool) => toolRow(tool, activeNames)).filter((row) => row.name);
|
|
2726
|
-
const tools = needle
|
|
2727
|
-
? rows.filter((row) => toolSearchMatches(row, needle))
|
|
2728
|
-
: rows;
|
|
2729
|
-
return {
|
|
2730
|
-
mode,
|
|
2731
|
-
count: rows.length,
|
|
2732
|
-
activeCount: rows.filter((row) => row.active).length,
|
|
2733
|
-
tools,
|
|
2734
|
-
activeTools: sortedNamesByMeasuredUsage(activeNames),
|
|
2735
|
-
discoveredTools: sortedNamesByMeasuredUsage(surface?.deferredDiscoveredTools || []),
|
|
2736
|
-
};
|
|
2737
|
-
},
|
|
2738
|
-
selectTools(names) {
|
|
2739
|
-
const list = Array.isArray(names) ? names : String(names || '').split(/[,\s]+/);
|
|
2740
|
-
const result = selectDeferredTools(activeToolSurface(), list, mode);
|
|
2741
|
-
return { ...result, status: this.toolsStatus() };
|
|
2742
|
-
},
|
|
2743
|
-
setCwd(path) {
|
|
2744
|
-
applyResolvedCwd(resolveCwdPath(path));
|
|
2745
|
-
return currentCwd;
|
|
2746
|
-
},
|
|
2747
|
-
mcpStatus() {
|
|
2748
|
-
return mcpStatus();
|
|
2749
|
-
},
|
|
2750
|
-
async reconnectMcp() {
|
|
2751
|
-
reloadFullConfig();
|
|
2752
|
-
const status = await connectConfiguredMcp({ reset: true });
|
|
2753
|
-
invalidatePreSessionToolSurface();
|
|
2754
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-reconnect');
|
|
2755
|
-
await recreateCurrentSessionIfReady();
|
|
2756
|
-
return status;
|
|
2757
|
-
},
|
|
2758
|
-
async addMcpServer(input = {}) {
|
|
2759
|
-
const { name, config: serverConfig } = normalizeMcpServerInput(input);
|
|
2760
|
-
const nextConfig = { ...config };
|
|
2761
|
-
nextConfig.mcpServers = {
|
|
2762
|
-
...(nextConfig.mcpServers || {}),
|
|
2763
|
-
[name]: serverConfig,
|
|
2764
|
-
};
|
|
2765
|
-
saveConfigAndAdopt(nextConfig);
|
|
2766
|
-
const status = await connectConfiguredMcp({ reset: true });
|
|
2767
|
-
invalidatePreSessionToolSurface();
|
|
2768
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-add');
|
|
2769
|
-
await recreateCurrentSessionIfReady();
|
|
2770
|
-
return { name, status };
|
|
2771
|
-
},
|
|
2772
|
-
async removeMcpServer(name) {
|
|
2773
|
-
const serverName = clean(name);
|
|
2774
|
-
if (!serverName) throw new Error('MCP server name is required');
|
|
2775
|
-
const nextConfig = { ...config };
|
|
2776
|
-
const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
|
|
2777
|
-
? { ...nextConfig.mcpServers }
|
|
2778
|
-
: {};
|
|
2779
|
-
if (!Object.prototype.hasOwnProperty.call(current, serverName)) {
|
|
2780
|
-
throw new Error(`MCP server not configured: ${serverName}`);
|
|
2781
|
-
}
|
|
2782
|
-
delete current[serverName];
|
|
2783
|
-
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
2784
|
-
const status = await connectConfiguredMcp({ reset: true });
|
|
2785
|
-
invalidatePreSessionToolSurface();
|
|
2786
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-remove');
|
|
2787
|
-
await recreateCurrentSessionIfReady();
|
|
2788
|
-
return status;
|
|
2789
|
-
},
|
|
2790
|
-
async setMcpServerEnabled(name, enabled) {
|
|
2791
|
-
const serverName = clean(name);
|
|
2792
|
-
if (!serverName) throw new Error('MCP server name is required');
|
|
2793
|
-
const nextConfig = { ...config };
|
|
2794
|
-
const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
|
|
2795
|
-
? { ...nextConfig.mcpServers }
|
|
2796
|
-
: {};
|
|
2797
|
-
if (!Object.prototype.hasOwnProperty.call(current, serverName)) {
|
|
2798
|
-
throw new Error(`MCP server not configured: ${serverName}`);
|
|
2799
|
-
}
|
|
2800
|
-
current[serverName] = { ...(current[serverName] || {}), enabled: enabled !== false };
|
|
2801
|
-
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
2802
|
-
const status = await connectConfiguredMcp({ reset: true });
|
|
2803
|
-
invalidatePreSessionToolSurface();
|
|
2804
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-mcp-toggle');
|
|
2805
|
-
await recreateCurrentSessionIfReady();
|
|
2806
|
-
return status;
|
|
2807
|
-
},
|
|
2808
|
-
skillsStatus() {
|
|
2809
|
-
return skillsStatus();
|
|
2810
|
-
},
|
|
2811
|
-
skillContent(name) {
|
|
2812
|
-
return skillContent(name);
|
|
2813
|
-
},
|
|
2814
|
-
async addSkill(input = {}) {
|
|
2815
|
-
const skill = addProjectSkill(input);
|
|
2816
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-skill-add');
|
|
2817
|
-
await recreateCurrentSessionIfReady();
|
|
2818
|
-
return { skill, status: skillsStatus() };
|
|
2819
|
-
},
|
|
2820
|
-
async reloadSkills() {
|
|
2821
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-skills-reload');
|
|
2822
|
-
await recreateCurrentSessionIfReady();
|
|
2823
|
-
return skillsStatus();
|
|
2824
|
-
},
|
|
2825
|
-
pluginsStatus() {
|
|
2826
|
-
return pluginsStatus();
|
|
2827
|
-
},
|
|
2828
|
-
async reloadPlugins() {
|
|
2829
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-plugins-reload');
|
|
2830
|
-
await recreateCurrentSessionIfReady();
|
|
2831
|
-
return pluginsStatus();
|
|
2832
|
-
},
|
|
2833
|
-
async addPlugin(source) {
|
|
2834
|
-
const dataDir = cfgMod.getPluginData?.();
|
|
2835
|
-
const plugin = registryAddPlugin(source, { dataDir });
|
|
2836
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-plugin-add');
|
|
2837
|
-
await recreateCurrentSessionIfReady();
|
|
2838
|
-
return { plugin, status: pluginsStatus() };
|
|
2839
|
-
},
|
|
2840
|
-
async updatePlugin(plugin = {}) {
|
|
2841
|
-
const key = clean(plugin.id || plugin.name || plugin);
|
|
2842
|
-
const dataDir = cfgMod.getPluginData?.();
|
|
2843
|
-
const updated = registryUpdatePlugin(key, { dataDir });
|
|
2844
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-plugin-update');
|
|
2845
|
-
await recreateCurrentSessionIfReady();
|
|
2846
|
-
return { plugin: updated, status: pluginsStatus() };
|
|
2847
|
-
},
|
|
2848
|
-
async removePlugin(plugin = {}) {
|
|
2849
|
-
const key = clean(plugin.id || plugin.name || plugin);
|
|
2850
|
-
const dataDir = cfgMod.getPluginData?.();
|
|
2851
|
-
const removed = registryRemovePlugin(key, { dataDir });
|
|
2852
|
-
const nextConfig = { ...config };
|
|
2853
|
-
const serverName = pluginMcpServerName(plugin);
|
|
2854
|
-
const prefix = `${serverName}--`;
|
|
2855
|
-
const hasMatch = nextConfig.mcpServers && Object.keys(nextConfig.mcpServers).some(
|
|
2856
|
-
(k) => k === serverName || k.startsWith(prefix)
|
|
2857
|
-
);
|
|
2858
|
-
if (hasMatch) {
|
|
2859
|
-
const current = { ...nextConfig.mcpServers };
|
|
2860
|
-
for (const k of Object.keys(current)) {
|
|
2861
|
-
if (k === serverName || k.startsWith(prefix)) delete current[k];
|
|
2862
|
-
}
|
|
2863
|
-
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
2864
|
-
await connectConfiguredMcp({ reset: true });
|
|
2865
|
-
invalidatePreSessionToolSurface();
|
|
2866
|
-
}
|
|
2867
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-plugin-remove');
|
|
2868
|
-
await recreateCurrentSessionIfReady();
|
|
2869
|
-
return { plugin: removed, status: pluginsStatus() };
|
|
2870
|
-
},
|
|
2871
|
-
async enablePluginMcp(plugin = {}) {
|
|
2872
|
-
const root = clean(plugin.root);
|
|
2873
|
-
const script = pluginMcpEnableScript(root, plugin);
|
|
2874
|
-
if (!root || !script) throw new Error('plugin has no MCP script');
|
|
2875
|
-
const serverName = pluginMcpServerName(plugin);
|
|
2876
|
-
const nextConfig = { ...config };
|
|
2877
|
-
const manifestMcp = pluginRawMcpServers(root, script);
|
|
2878
|
-
if (manifestMcp) {
|
|
2879
|
-
const { rawServers, mcpRoot } = manifestMcp;
|
|
2880
|
-
const keys = Object.keys(rawServers).filter((k) => {
|
|
2881
|
-
const v = rawServers[k];
|
|
2882
|
-
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
2883
|
-
});
|
|
2884
|
-
const ownedPrefix = `${serverName}--`;
|
|
2885
|
-
const nextServers = {};
|
|
2886
|
-
for (const [k, v] of Object.entries(nextConfig.mcpServers || {})) {
|
|
2887
|
-
if (k === serverName || k.startsWith(ownedPrefix)) continue;
|
|
2888
|
-
nextServers[k] = v;
|
|
2889
|
-
}
|
|
2890
|
-
for (const serverKey of keys) {
|
|
2891
|
-
const cfg = normalizePluginMcpServerConfig(rawServers[serverKey], mcpRoot);
|
|
2892
|
-
cfg.env = {
|
|
2893
|
-
...(cfg.env || {}),
|
|
2894
|
-
MIXDOG_PLUGIN_ROOT: root,
|
|
2895
|
-
MIXDOG_PLUGIN_DATA: join(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR, 'plugins', 'data', clean(plugin.id || plugin.name || serverName)),
|
|
2896
|
-
};
|
|
2897
|
-
const key = keys.length === 1 ? serverName : `${serverName}--${serverKey}`;
|
|
2898
|
-
nextServers[key] = cfg;
|
|
2899
|
-
}
|
|
2900
|
-
nextConfig.mcpServers = nextServers;
|
|
2901
|
-
} else {
|
|
2902
|
-
const scriptPath = resolveContainedPluginPath(root, script);
|
|
2903
|
-
if (!scriptPath || !existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${join(root, script)}`);
|
|
2904
|
-
nextConfig.mcpServers = {
|
|
2905
|
-
...(nextConfig.mcpServers || {}),
|
|
2906
|
-
[serverName]: {
|
|
2907
|
-
command: 'node',
|
|
2908
|
-
args: [scriptPath],
|
|
2909
|
-
cwd: root,
|
|
2910
|
-
env: {
|
|
2911
|
-
MIXDOG_PLUGIN_ROOT: root,
|
|
2912
|
-
MIXDOG_PLUGIN_DATA: join(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR, 'plugins', 'data', clean(plugin.id || plugin.name || serverName)),
|
|
2913
|
-
},
|
|
2914
|
-
},
|
|
2915
|
-
};
|
|
2916
|
-
}
|
|
2917
|
-
saveConfigAndAdopt(nextConfig);
|
|
2918
|
-
const status = await connectConfiguredMcp({ reset: true });
|
|
2919
|
-
invalidatePreSessionToolSurface();
|
|
2920
|
-
if (session?.id) mgr.closeSession(session.id, 'cli-plugin-mcp-enable');
|
|
2921
|
-
await recreateCurrentSessionIfReady();
|
|
2922
|
-
return { serverName, status };
|
|
2923
|
-
},
|
|
2924
|
-
hooksStatus() {
|
|
2925
|
-
return hooks.status();
|
|
2926
|
-
},
|
|
2927
|
-
addHookRule(rule) {
|
|
2928
|
-
return hooks.addRule(rule);
|
|
2929
|
-
},
|
|
2930
|
-
setHookRuleEnabled(index, enabled) {
|
|
2931
|
-
return hooks.setRuleEnabled(index, enabled);
|
|
2932
|
-
},
|
|
2933
|
-
deleteHookRule(index) {
|
|
2934
|
-
return hooks.deleteRule(index);
|
|
2935
|
-
},
|
|
2936
|
-
async memoryControl(args = {}) {
|
|
2937
|
-
const memoryMod = await getMemoryModule();
|
|
2938
|
-
if (!memoryMod?.handleToolCall) throw new Error('memory runtime is not available');
|
|
2939
|
-
return toolResponseText(await memoryMod.handleToolCall('memory', args || {}));
|
|
2940
|
-
},
|
|
2941
|
-
async recall(query, args = {}) {
|
|
2942
|
-
const baseQuery = query || args?.query || '';
|
|
2943
|
-
if (args?.currentSession !== false && session?.id) {
|
|
2944
|
-
const currentText = currentSessionRecallRows(session, baseQuery, { limit: args?.limit });
|
|
2945
|
-
if (!isEmptyRecallText(currentText)) return currentText;
|
|
2946
|
-
}
|
|
2947
|
-
const memoryMod = await getMemoryModule();
|
|
2948
|
-
if (!memoryMod?.handleToolCall) throw new Error('memory runtime is not available');
|
|
2949
|
-
const baseArgs = {
|
|
2950
|
-
...(args || {}),
|
|
2951
|
-
query: baseQuery,
|
|
2952
|
-
cwd: args?.cwd || currentCwd,
|
|
2953
|
-
// Grouping hint for multi-session browse output: lets the memory
|
|
2954
|
-
// service mark THIS session's group as "(current)". Not a filter.
|
|
2955
|
-
...(session?.id ? { currentSessionId: session.id } : {}),
|
|
2956
|
-
};
|
|
2957
|
-
let result = '(no results)';
|
|
2958
|
-
if (session?.id && args?.currentSession !== false && args?.forceCycleOnEmpty !== false) {
|
|
2959
|
-
// Empty-fallback: hydrate the current transcript into the memory DB
|
|
2960
|
-
// (no LLM — ingest only) and re-search session-scoped with the raw
|
|
2961
|
-
// leg on. The old path also ran a synchronous cycle1 (LLM chunking)
|
|
2962
|
-
// here, which made an empty recall the slowest possible recall and
|
|
2963
|
-
// called the LLM even with recap/memory off; the raw rows are
|
|
2964
|
-
// searchable directly (FTS/trgm + post-ingest raw embeddings), so
|
|
2965
|
-
// chunking is left to the background cycle.
|
|
2966
|
-
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
2967
|
-
if (messages.length > 0) {
|
|
2968
|
-
await memoryMod.handleToolCall('memory', {
|
|
2969
|
-
action: 'ingest_session',
|
|
2970
|
-
sessionId: session.id,
|
|
2971
|
-
cwd: currentCwd,
|
|
2972
|
-
messages,
|
|
2973
|
-
});
|
|
2974
|
-
result = toolResponseText(await memoryMod.handleToolCall('recall', {
|
|
2975
|
-
...baseArgs,
|
|
2976
|
-
sessionId: session.id,
|
|
2977
|
-
currentSession: true,
|
|
2978
|
-
projectScope: baseArgs.projectScope || 'all',
|
|
2979
|
-
includeRaw: baseArgs.includeRaw !== false,
|
|
2980
|
-
includeArchived: baseArgs.includeArchived !== false,
|
|
2981
|
-
}));
|
|
2982
|
-
}
|
|
2983
|
-
}
|
|
2984
|
-
if (isEmptyRecallText(result)) {
|
|
2985
|
-
result = toolResponseText(await memoryMod.handleToolCall('recall', baseArgs));
|
|
2986
|
-
}
|
|
2987
|
-
return result;
|
|
2988
|
-
},
|
|
2989
|
-
async setRoute(next, options = {}) {
|
|
2990
|
-
// Model/provider changes take effect on the NEXT session only — never
|
|
2991
|
-
// rewrite a running session's provider/model in place. A live turn's
|
|
2992
|
-
// prompt cache (Anthropic/OpenAI/etc.) is provider-keyed; flipping
|
|
2993
|
-
// session.provider mid-conversation forces a full cache-miss rewrite
|
|
2994
|
-
// of the entire history on the very next turn (seen as a promptΔ
|
|
2995
|
-
// spike + cache_ratio=0% in session-bench). `route` (this closure
|
|
2996
|
-
// variable) still updates immediately so the NEXT createCurrentSession()
|
|
2997
|
-
// picks it up; only the currently-open session is left untouched.
|
|
2998
|
-
const applyToCurrentSession = options?.applyToCurrentSession === true;
|
|
2999
|
-
const requested = { ...(next || {}) };
|
|
3000
|
-
validateRequestedModelSelector(config, requested);
|
|
3001
|
-
const providerExplicitlyRequested = clean(next?.provider) !== '';
|
|
3002
|
-
if (requested.effort === undefined && !requested.provider && !requested.model && hasOwn(route, 'effort')) {
|
|
3003
|
-
requested.effort = route.effort;
|
|
3004
|
-
}
|
|
3005
|
-
if (!requested.provider && requested.model && !findPreset(config, requested.model)) {
|
|
3006
|
-
requested.provider = route.provider;
|
|
3007
|
-
}
|
|
3008
|
-
let selectedRoute = resolveRoute(config, requested);
|
|
3009
|
-
await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
|
|
3010
|
-
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
3011
|
-
// Guard (option A / strict reject): a free-text string must never become the
|
|
3012
|
-
// lead model. When the caller did not pin a provider, the route did not
|
|
3013
|
-
// match a preset, and the model is unknown to both the live provider
|
|
3014
|
-
// catalog (modelMeta is still the {id,provider} placeholder) and the
|
|
3015
|
-
// offline catalog, refuse BEFORE any config write so a partial save
|
|
3016
|
-
// (saveModelSettings/adoptConfig/persistLeadRoute) can never land.
|
|
3017
|
-
if (!providerExplicitlyRequested
|
|
3018
|
-
&& !selectedRoute.preset
|
|
3019
|
-
&& !modelMetaLooksResolved(modelMeta)
|
|
3020
|
-
&& !getModelMetadataSync(selectedRoute.model, selectedRoute.provider)) {
|
|
3021
|
-
throw new Error(`unknown model: ${selectedRoute.provider}/${selectedRoute.model}`);
|
|
3022
|
-
}
|
|
3023
|
-
const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
|
|
3024
|
-
selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
|
|
3025
|
-
adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
3026
|
-
const leadRoute = persistLeadRoute(selectedRoute);
|
|
3027
|
-
route = resolveRoute(config, leadRoute
|
|
3028
|
-
? { model: workflowPresetId('lead') }
|
|
3029
|
-
: selectedRoute);
|
|
3030
|
-
await refreshRouteEffort(modelMeta);
|
|
3031
|
-
refreshStatuslineUsageSnapshot(route);
|
|
3032
|
-
scheduleStatuslineUsageRefresh();
|
|
3033
|
-
if (!applyToCurrentSession) {
|
|
3034
|
-
// `route` is updated for the next session; the open session (and its
|
|
3035
|
-
// live provider/model/cache chain) is left alone on purpose.
|
|
3036
|
-
return route;
|
|
3037
|
-
}
|
|
3038
|
-
if (session) {
|
|
3039
|
-
const updated = mgr.updateSessionRoute?.(session.id, {
|
|
3040
|
-
provider: route.provider,
|
|
3041
|
-
model: route.model,
|
|
3042
|
-
fast: route.fast === true,
|
|
3043
|
-
effort: route.effectiveEffort || null,
|
|
3044
|
-
});
|
|
3045
|
-
if (updated) session = updated;
|
|
3046
|
-
else {
|
|
3047
|
-
session.provider = route.provider;
|
|
3048
|
-
session.model = route.model;
|
|
3049
|
-
session.fast = route.fast === true;
|
|
3050
|
-
session.effort = route.effectiveEffort || null;
|
|
3051
|
-
}
|
|
3052
|
-
writeStatuslineRoute(statusRoutes, session, route);
|
|
3053
|
-
invalidateContextStatusCache();
|
|
3054
|
-
}
|
|
3055
|
-
return route;
|
|
3056
|
-
},
|
|
3057
|
-
|
|
3058
|
-
async setFast(value) {
|
|
3059
|
-
const enabled = value === true;
|
|
3060
|
-
const modelMeta = await lookupModelMeta(route.provider, route.model);
|
|
3061
|
-
const fastCapable = fastCapableFor(route.provider, modelMeta);
|
|
3062
|
-
if (enabled && !fastCapable) {
|
|
3063
|
-
throw new Error(`fast mode is not available for ${route.provider}/${route.model}`);
|
|
3064
|
-
}
|
|
3065
|
-
route = resolveRoute(config, { provider: route.provider, model: route.model, effort: route.effort, fast: fastCapable ? enabled : false });
|
|
3066
|
-
adoptConfig(saveModelSettings(cfgMod, route, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
3067
|
-
const leadRoute = persistLeadRoute(route);
|
|
3068
|
-
if (leadRoute) route = resolveRoute(config, { model: workflowPresetId('lead') });
|
|
3069
|
-
await refreshRouteEffort(modelMeta);
|
|
3070
|
-
if (session) {
|
|
3071
|
-
session.fast = route.fast === true;
|
|
3072
|
-
session.effort = route.effectiveEffort || null;
|
|
3073
|
-
writeStatuslineRoute(statusRoutes, session, route);
|
|
3074
|
-
invalidateContextStatusCache();
|
|
3075
|
-
}
|
|
3076
|
-
return route.fast === true;
|
|
3077
|
-
},
|
|
3078
|
-
|
|
3079
|
-
async toggleFast() {
|
|
3080
|
-
return await this.setFast(!(route.fast === true));
|
|
3081
|
-
},
|
|
3082
|
-
|
|
3083
|
-
async setEffort(value) {
|
|
3084
|
-
const normalized = normalizeEffortInput(value);
|
|
3085
|
-
route = { ...route, effort: normalized };
|
|
3086
|
-
const modelMeta = await lookupModelMeta(route.provider, route.model);
|
|
3087
|
-
const fastCapable = fastCapableFor(route.provider, modelMeta);
|
|
3088
|
-
adoptConfig(saveModelSettings(cfgMod, route, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
|
|
3089
|
-
const leadRoute = persistLeadRoute(route);
|
|
3090
|
-
if (leadRoute) {
|
|
3091
|
-
route = resolveRoute(config, { model: workflowPresetId('lead') });
|
|
3092
|
-
}
|
|
3093
|
-
await refreshRouteEffort(modelMeta);
|
|
3094
|
-
if (session) {
|
|
3095
|
-
session.effort = route.effectiveEffort || null;
|
|
3096
|
-
writeStatuslineRoute(statusRoutes, session, route);
|
|
3097
|
-
invalidateContextStatusCache();
|
|
3098
|
-
}
|
|
3099
|
-
return route;
|
|
3100
|
-
},
|
|
3101
|
-
async close(reason = 'cli-exit', options = {}) {
|
|
3102
|
-
const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
|
|
3103
|
-
closeRequested = true;
|
|
3104
|
-
// SessionEnd: bridge teardown to the standard hook bus. reason mapped to
|
|
3105
|
-
// standard values ('clear'/'exit' where applicable, else 'other'). Short
|
|
3106
|
-
// await guard so a slow hook cannot wedge teardown; best-effort.
|
|
3107
|
-
try {
|
|
3108
|
-
const rl = String(reason || '').toLowerCase();
|
|
3109
|
-
const endReason = /clear/.test(rl) ? 'clear'
|
|
3110
|
-
: /exit|quit|cli-exit|shutdown|sigint|sigterm/.test(rl) ? 'exit'
|
|
3111
|
-
: 'other';
|
|
3112
|
-
if (session?.id) {
|
|
3113
|
-
await withTeardownDeadline(
|
|
3114
|
-
Promise.resolve(hooks.dispatch('SessionEnd', hookCommonPayload({ session_id: session.id, reason: endReason }))).catch(() => {}),
|
|
3115
|
-
300,
|
|
3116
|
-
undefined,
|
|
3117
|
-
);
|
|
3118
|
-
}
|
|
3119
|
-
} catch { /* best-effort: SessionEnd hook must never wedge teardown */ }
|
|
3120
|
-
// Persist any change that is still sitting in the debounce window so a
|
|
3121
|
-
// toggle made right before exit is not lost. Synchronous + best-effort:
|
|
3122
|
-
// teardown must continue even if the final write fails.
|
|
3123
|
-
try { flushConfigSave(); } catch {}
|
|
3124
|
-
try { flushBackendSave(); } catch {}
|
|
3125
|
-
try { flushOutputStyleSave(); } catch {}
|
|
3126
|
-
if (prewarmTimers.channelStartTimer) {
|
|
3127
|
-
clearTimeout(prewarmTimers.channelStartTimer);
|
|
3128
|
-
prewarmTimers.channelStartTimer = null;
|
|
3129
|
-
}
|
|
3130
|
-
for (const timerKey of [
|
|
3131
|
-
'providerSetupWarmupTimer',
|
|
3132
|
-
'providerWarmupTimer',
|
|
3133
|
-
'providerModelWarmupTimer',
|
|
3134
|
-
'modelCatalogWarmupTimer',
|
|
3135
|
-
]) {
|
|
3136
|
-
if (warmupTimers[timerKey]) {
|
|
3137
|
-
clearTimeout(warmupTimers[timerKey]);
|
|
3138
|
-
warmupTimers[timerKey] = null;
|
|
3139
|
-
}
|
|
3140
|
-
}
|
|
3141
|
-
if (prewarmTimers.codeGraphPrewarmTimer) {
|
|
3142
|
-
clearTimeout(prewarmTimers.codeGraphPrewarmTimer);
|
|
3143
|
-
prewarmTimers.codeGraphPrewarmTimer = null;
|
|
3144
|
-
}
|
|
3145
|
-
for (const timerKey of ['statuslineUsageWarmupTimer', 'statuslineUsageRefreshTimer']) {
|
|
3146
|
-
if (warmupTimers[timerKey]) {
|
|
3147
|
-
clearTimeout(warmupTimers[timerKey]);
|
|
3148
|
-
warmupTimers[timerKey] = null;
|
|
3149
|
-
}
|
|
3150
|
-
}
|
|
3151
|
-
try { cancelBackgroundTasks({ reason, notify: false }); } catch {}
|
|
3152
|
-
const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
|
|
3153
|
-
try { agentTool.closeAll(reason); } catch {}
|
|
3154
|
-
let mcpStop = null;
|
|
3155
|
-
try { mcpStop = mcpClient.disconnectAll?.(); } catch {}
|
|
3156
|
-
const openaiWsStop = globalThis.__mixdogOpenaiWsRuntimeLoaded === true
|
|
3157
|
-
? import('./runtime/agent/orchestrator/providers/openai-oauth-ws.mjs')
|
|
3158
|
-
.then((mod) => mod?.drainOpenaiWsPool?.(reason))
|
|
3159
|
-
.catch(() => {})
|
|
3160
|
-
: null;
|
|
3161
|
-
const patchStop = closePatchRuntimeIfLoaded(detach ? { waitForExit: false } : undefined);
|
|
3162
|
-
const memoryStop = memoryModPromise
|
|
3163
|
-
? memoryModPromise
|
|
3164
|
-
.then((mod) => (typeof mod?.stop === 'function' ? mod.stop() : null))
|
|
3165
|
-
.catch(() => {})
|
|
3166
|
-
.finally(() => {
|
|
3167
|
-
memoryModPromise = null;
|
|
3168
|
-
})
|
|
3169
|
-
: null;
|
|
3170
|
-
let ok = false;
|
|
3171
|
-
if (session?.id) {
|
|
3172
|
-
statusRoutes?.clearGatewaySessionRoute?.(session.id);
|
|
3173
|
-
// Bug fix: runtime stop/exit (TUI Ctrl-C, process exit) previously
|
|
3174
|
-
// always tombstoned the current session, so a session you were
|
|
3175
|
-
// mid-conversation in vanished from the Resume list the instant you
|
|
3176
|
-
// quit and was hard-deleted by the 24h tombstone sweep. Only
|
|
3177
|
-
// tombstone truly-empty scratch sessions; non-empty sessions must
|
|
3178
|
-
// survive exit resumable.
|
|
3179
|
-
// liveTurnMessages holds the in-flight user prompt until turn
|
|
3180
|
-
// commit — an active first-turn ask has its user message there,
|
|
3181
|
-
// not yet in session.messages, so it must also be checked or a
|
|
3182
|
-
// first-turn exit could still burn a real session.
|
|
3183
|
-
const tombstone = !hasUserConversationMessage(session.messages)
|
|
3184
|
-
&& !hasUserConversationMessage(session.liveTurnMessages);
|
|
3185
|
-
ok = mgr.closeSession(session.id, reason, { tombstone });
|
|
3186
|
-
session = null;
|
|
3187
|
-
}
|
|
3188
|
-
const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
|
|
3189
|
-
? import('./runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs')
|
|
3190
|
-
.then((mod) => mod?.shutdownShellJobs?.(reason, { sync: !detach }))
|
|
3191
|
-
.catch(() => {})
|
|
3192
|
-
: null;
|
|
3193
|
-
const bashSessionsStop = globalThis.__mixdogBashSessionRuntimeLoaded === true
|
|
3194
|
-
? import('./runtime/agent/orchestrator/tools/bash-session.mjs')
|
|
3195
|
-
.then((mod) => mod?.shutdownBashSessions?.(reason))
|
|
3196
|
-
.catch(() => {})
|
|
3197
|
-
: null;
|
|
3198
|
-
if (detach) {
|
|
3199
|
-
try { await withTeardownDeadline(channelStop, 300, false); } catch {}
|
|
3200
|
-
try { await withTeardownDeadline(shellJobsStop, 300, false); } catch {}
|
|
3201
|
-
try { await withTeardownDeadline(bashSessionsStop, 300, false); } catch {}
|
|
3202
|
-
try { await withTeardownDeadline(memoryStop, 1500, false); } catch {}
|
|
3203
|
-
for (const stop of [mcpStop, openaiWsStop, patchStop]) {
|
|
3204
|
-
Promise.resolve(stop).catch(() => {});
|
|
3205
|
-
}
|
|
3206
|
-
return ok;
|
|
3207
|
-
}
|
|
3208
|
-
await Promise.allSettled([
|
|
3209
|
-
withTeardownDeadline(channelStop, 5500, false),
|
|
3210
|
-
withTeardownDeadline(mcpStop, 1500, false),
|
|
3211
|
-
withTeardownDeadline(openaiWsStop, 1500, false),
|
|
3212
|
-
withTeardownDeadline(patchStop, 1500, false),
|
|
3213
|
-
withTeardownDeadline(memoryStop, 5500, false),
|
|
3214
|
-
withTeardownDeadline(shellJobsStop, 1500, false),
|
|
3215
|
-
withTeardownDeadline(bashSessionsStop, 1500, false),
|
|
3216
|
-
]);
|
|
3217
|
-
return ok;
|
|
3218
|
-
},
|
|
3219
|
-
abort(reason = 'cli-abort') {
|
|
3220
|
-
if (!session?.id) return false;
|
|
3221
|
-
return mgr.abortSessionTurn(session.id, reason);
|
|
3222
|
-
},
|
|
3223
|
-
listSessions() {
|
|
3224
|
-
return mgr.listSessions({}).map(s => {
|
|
3225
|
-
const owner = clean(s.owner || 'user').toLowerCase();
|
|
3226
|
-
if (owner && !['cli', 'user', 'mixdog', 'legacy'].includes(owner)) return null;
|
|
3227
|
-
const sourceType = clean(s.sourceType || '').toLowerCase();
|
|
3228
|
-
const sourceName = clean(s.sourceName || '').toLowerCase();
|
|
3229
|
-
const agent = clean(s.agent || '').toLowerCase();
|
|
3230
|
-
const leadish = agent === 'lead'
|
|
3231
|
-
|| sourceType === 'lead'
|
|
3232
|
-
// Bug fix: side-terminal cli sessions have a non-empty/non-'main' sourceName
|
|
3233
|
-
// (e.g. terminal id) and were being hidden from resume even though they are
|
|
3234
|
-
// legitimate user sessions, not agent-owned. Any sourceName is fine for cli.
|
|
3235
|
-
|| (sourceType === 'cli')
|
|
3236
|
-
|| (!sourceType && !sourceName && !isAgentOwner(owner));
|
|
3237
|
-
if (!leadish) return null;
|
|
3238
|
-
let preview = cleanSessionPreview(s.preview || '');
|
|
3239
|
-
let messageCount = Math.max(0, Number(s.messageCount) || 0);
|
|
3240
|
-
if (!preview && Array.isArray(s.messages)) {
|
|
3241
|
-
const msgs = s.messages || [];
|
|
3242
|
-
const userPreviews = msgs
|
|
3243
|
-
.filter(m => m && m.role === 'user')
|
|
3244
|
-
.map(m => cleanSessionPreview(sessionMessageText(m.content)))
|
|
3245
|
-
.filter(text => !isSessionPreviewNoise(text));
|
|
3246
|
-
preview = userPreviews[userPreviews.length - 1] || userPreviews[0] || '';
|
|
3247
|
-
messageCount = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant')).length;
|
|
3248
|
-
}
|
|
3249
|
-
// Bug fix: sessions whose preview couldn't be derived (e.g. noise-only user
|
|
3250
|
-
// turns) were silently dropped from the resume list even when they had real
|
|
3251
|
-
// messages. Keep the row and let preview fall back to '' (TUI renders
|
|
3252
|
-
// '(no message)' for empty preview); only drop truly-empty scratch sessions
|
|
3253
|
-
// with zero visible messages.
|
|
3254
|
-
if (!preview && messageCount === 0) return null;
|
|
3255
|
-
return {
|
|
3256
|
-
id: s.id,
|
|
3257
|
-
updatedAt: s.updatedAt,
|
|
3258
|
-
cwd: s.cwd || '',
|
|
3259
|
-
model: s.model,
|
|
3260
|
-
provider: s.provider,
|
|
3261
|
-
messageCount,
|
|
3262
|
-
preview,
|
|
3263
|
-
};
|
|
3264
|
-
}).filter(Boolean);
|
|
3265
|
-
},
|
|
3266
|
-
async newSession() {
|
|
3267
|
-
if (session?.id) {
|
|
3268
|
-
// Bug fix: /new used to unconditionally tombstone the outgoing
|
|
3269
|
-
// session, so switching to a fresh session burned whatever you'd
|
|
3270
|
-
// been working on — it dropped off the Resume list immediately and
|
|
3271
|
-
// was hard-deleted after the 24h tombstone sweep. Only tombstone
|
|
3272
|
-
// truly-empty scratch sessions; keep non-empty ones resumable.
|
|
3273
|
-
const tombstone = !hasUserConversationMessage(session.messages)
|
|
3274
|
-
&& !hasUserConversationMessage(session.liveTurnMessages);
|
|
3275
|
-
mgr.closeSession(session.id, 'cli-new', { tombstone });
|
|
3276
|
-
// Bug fix: closeSession({tombstone:false}) keeps the outgoing
|
|
3277
|
-
// session file intact so it stays resumable — but that meant
|
|
3278
|
-
// createCurrentSession()'s live-session reuse check (session?.id →
|
|
3279
|
-
// mgr.getSession(...) not closed) happily reloaded the SAME session,
|
|
3280
|
-
// so /new never actually reset the transcript and the context gauge
|
|
3281
|
-
// never returned to 0. Drop the in-memory reference so a fresh
|
|
3282
|
-
// session is always created.
|
|
3283
|
-
session = null;
|
|
3284
|
-
}
|
|
3285
|
-
invalidateContextStatusCache();
|
|
3286
|
-
await createCurrentSession();
|
|
3287
|
-
return session.id;
|
|
3288
|
-
},
|
|
3289
|
-
async resume(id) {
|
|
3290
|
-
const previousId = session?.id || null;
|
|
3291
|
-
const previousMessages = session?.messages || null;
|
|
3292
|
-
const previousLive = session?.liveTurnMessages || null;
|
|
3293
|
-
const resumed = await mgr.resumeSession(id, toolSpecForMode(mode));
|
|
3294
|
-
if (!resumed) return null;
|
|
3295
|
-
if (previousId && previousId !== resumed.id) {
|
|
3296
|
-
statusRoutes?.clearGatewaySessionRoute?.(previousId);
|
|
3297
|
-
// Bug fix: /resume used to unconditionally tombstone the session
|
|
3298
|
-
// you were switching away from, so it vanished from the Resume
|
|
3299
|
-
// list right away and was hard-deleted after the 24h sweep — the
|
|
3300
|
-
// exact "burn a resumable session" bug this fix targets. Only
|
|
3301
|
-
// tombstone truly-empty scratch sessions.
|
|
3302
|
-
const tombstone = !hasUserConversationMessage(previousMessages)
|
|
3303
|
-
&& !hasUserConversationMessage(previousLive);
|
|
3304
|
-
mgr.closeSession(previousId, 'cli-resume', { tombstone });
|
|
3305
|
-
}
|
|
3306
|
-
session = resumed;
|
|
3307
|
-
currentCwd = resumed.cwd || currentCwd;
|
|
3308
|
-
applyResolvedCwd(currentCwd, { markRefresh: false });
|
|
3309
|
-
const resumeEffort = hasOwn(route, 'effort') ? route.effort : resumed.effort;
|
|
3310
|
-
route = resolveRoute(config, { provider: resumed.provider, model: resumed.model, effort: resumeEffort });
|
|
3311
|
-
await refreshRouteEffort();
|
|
3312
|
-
session.effort = route.effectiveEffort || null;
|
|
3313
|
-
session.cwd = currentCwd;
|
|
3314
|
-
applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
3315
|
-
invalidatePreSessionToolSurface();
|
|
3316
|
-
invalidateContextStatusCache();
|
|
3317
|
-
sessionNeedsCwdRefresh = false;
|
|
3318
|
-
writeStatuslineRoute(statusRoutes, session, route);
|
|
3319
|
-
return {
|
|
3320
|
-
id: resumed.id,
|
|
3321
|
-
messages: resumed.messages || [],
|
|
3322
|
-
cwd: currentCwd,
|
|
3323
|
-
provider: resumed.provider,
|
|
3324
|
-
model: resumed.model,
|
|
3325
|
-
};
|
|
3326
|
-
},
|
|
3327
|
-
};
|
|
3328
|
-
}
|
|
1
|
+
// Thin facade. The former ~3.3k-line implementation was split into cohesive
|
|
2
|
+
// modules under src/session-runtime/ (runtime-core.mjs holds the orchestrator
|
|
3
|
+
// createMixdogSessionRuntime plus the module-level wiring; env.mjs and
|
|
4
|
+
// boot-profile.mjs hold the extracted env/boot helpers). This file re-exports
|
|
5
|
+
// the exact same public API so existing importers (TUI engine,
|
|
6
|
+
// scripts/tool-smoke.mjs, tests) keep their import paths unchanged.
|
|
7
|
+
export * from './session-runtime/runtime-core.mjs';
|