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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -34
  3. package/package.json +3 -2
  4. package/scripts/build-tui.mjs +6 -0
  5. package/scripts/hook-bus-test.mjs +8 -0
  6. package/scripts/log-writer-guard-smoke.mjs +131 -0
  7. package/scripts/reactive-compact-persist-smoke.mjs +22 -6
  8. package/scripts/recall-bench-cases.json +0 -1
  9. package/scripts/recall-quality-cases.json +1 -2
  10. package/scripts/session-ingest-compaction-smoke.mjs +241 -0
  11. package/scripts/tool-smoke.mjs +150 -45
  12. package/src/defaults/skills/setup/SKILL.md +327 -0
  13. package/src/help.mjs +2 -5
  14. package/src/lib/mixdog-debug.cjs +13 -0
  15. package/src/mixdog-session-runtime.mjs +7 -3328
  16. package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
  17. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
  18. package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
  19. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
  20. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
  21. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
  22. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
  23. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
  24. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
  25. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
  26. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
  27. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
  28. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
  29. package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
  30. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
  31. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
  32. package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
  33. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
  34. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
  35. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
  36. package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
  37. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
  38. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
  39. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
  40. package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
  41. package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
  42. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
  43. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
  44. package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
  45. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
  46. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
  47. package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
  48. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
  49. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
  50. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
  51. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
  52. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
  53. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
  54. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  55. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
  56. package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
  57. package/src/runtime/channels/index.mjs +6 -2183
  58. package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
  59. package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
  60. package/src/runtime/channels/lib/network-retry.mjs +23 -0
  61. package/src/runtime/channels/lib/owned-runtime.mjs +627 -0
  62. package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
  63. package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
  64. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
  65. package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
  66. package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
  67. package/src/runtime/channels/lib/worker-main.mjs +777 -0
  68. package/src/runtime/memory/index.mjs +163 -1725
  69. package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
  70. package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
  71. package/src/runtime/memory/lib/http-router.mjs +811 -0
  72. package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
  73. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
  74. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
  75. package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
  76. package/src/runtime/memory/lib/memory-embed.mjs +28 -7
  77. package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
  78. package/src/runtime/memory/lib/memory.mjs +39 -0
  79. package/src/runtime/memory/lib/query-handlers.mjs +2 -2
  80. package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
  81. package/src/runtime/shared/atomic-file.mjs +138 -80
  82. package/src/runtime/shared/child-guardian.mjs +61 -3
  83. package/src/session-runtime/boot-profile.mjs +36 -0
  84. package/src/session-runtime/channel-config-api.mjs +70 -0
  85. package/src/session-runtime/context-status.mjs +181 -0
  86. package/src/session-runtime/env.mjs +17 -0
  87. package/src/session-runtime/lifecycle-api.mjs +242 -0
  88. package/src/session-runtime/model-route-api.mjs +198 -0
  89. package/src/session-runtime/provider-auth-api.mjs +135 -0
  90. package/src/session-runtime/resource-api.mjs +282 -0
  91. package/src/session-runtime/runtime-core.mjs +2104 -0
  92. package/src/session-runtime/session-turn-api.mjs +274 -0
  93. package/src/session-runtime/tool-catalog.mjs +18 -264
  94. package/src/session-runtime/tool-defs.mjs +2 -2
  95. package/src/session-runtime/workflow-agents-api.mjs +238 -0
  96. package/src/standalone/agent-tool.mjs +2 -2
  97. package/src/standalone/channel-worker.mjs +67 -7
  98. package/src/standalone/folder-dialog.mjs +4 -1
  99. package/src/standalone/memory-runtime-proxy.mjs +154 -17
  100. package/src/standalone/seeds.mjs +28 -1
  101. package/src/tui/App.jsx +40 -28
  102. package/src/tui/app/core-memory-picker.mjs +1 -1
  103. package/src/tui/app/doctor.mjs +175 -0
  104. package/src/tui/app/slash-commands.mjs +1 -0
  105. package/src/tui/app/slash-dispatch.mjs +9 -0
  106. package/src/tui/app/use-mouse-input.mjs +6 -0
  107. package/src/tui/app/use-transcript-scroll.mjs +77 -3
  108. package/src/tui/dist/index.mjs +2851 -2162
  109. package/src/tui/engine/context-state.mjs +145 -0
  110. package/src/tui/engine/prompt-history.mjs +27 -0
  111. package/src/tui/engine/session-api-ext.mjs +478 -0
  112. package/src/tui/engine/session-api.mjs +564 -0
  113. package/src/tui/engine/session-flow.mjs +485 -0
  114. package/src/tui/engine/turn.mjs +1078 -0
  115. package/src/tui/engine.mjs +68 -2620
  116. package/src/tui/index.jsx +7 -0
  117. package/vendor/ink/build/ink.js +16 -1
  118. package/vendor/ink/build/output.js +30 -4
  119. package/vendor/ink/build/render.js +5 -0
  120. package/src/workflows/sequential/WORKFLOW.md +0 -51
@@ -0,0 +1,17 @@
1
+ // Environment-variable coercion helpers shared by the session runtime.
2
+ // Extracted verbatim from mixdog-session-runtime.mjs during the facade split.
3
+ export function envFlag(name) {
4
+ return /^(1|true|yes|on)$/i.test(String(process.env[name] || ''));
5
+ }
6
+
7
+ export function envPresent(name) {
8
+ return process.env[name] !== undefined && process.env[name] !== '';
9
+ }
10
+
11
+ export function envDelayMs(name, fallback, { min = 0, max = 60_000 } = {}) {
12
+ const raw = process.env[name];
13
+ if (raw === undefined || raw === '') return fallback;
14
+ const n = Number(raw);
15
+ if (!Number.isFinite(n)) return fallback;
16
+ return Math.min(max, Math.max(min, Math.floor(n)));
17
+ }
@@ -0,0 +1,242 @@
1
+ import { cancelBackgroundTasks } from '../runtime/shared/background-tasks.mjs';
2
+ import { hasUserConversationMessage } from '../runtime/agent/orchestrator/session/manager/prompt-utils.mjs';
3
+ import { isAgentOwner } from '../runtime/agent/orchestrator/agent-owner.mjs';
4
+ import { writeStatuslineRoute } from './statusline-route.mjs';
5
+ import {
6
+ sessionMessageText,
7
+ isSessionPreviewNoise,
8
+ cleanSessionPreview,
9
+ clean,
10
+ hasOwn,
11
+ } from './session-text.mjs';
12
+ import { toolSpecForMode, deferredSurfaceModeForLead } from './effort.mjs';
13
+
14
+ // Session lifecycle surface: teardown (close/abort), resume/new, and the
15
+ // resumable-session listing. Extracted verbatim from the runtime API object;
16
+ // stateless helpers are imported directly and the runtime injects live
17
+ // getters/setters for the mutable session/route/cwd locals plus the closure
18
+ // callbacks and long-lived handles (managers, timers, channel/agent/mcp).
19
+ export function createLifecycleApi(deps) {
20
+ const {
21
+ getSession, setSession, getRoute, setRoute, getConfig, getMode, getCurrentCwd,
22
+ setCloseRequested, getMemoryModPromise, setMemoryModPromise,
23
+ setSessionNeedsCwdRefresh,
24
+ hooks, hookCommonPayload, mgr, statusRoutes, channels, agentTool, mcpClient,
25
+ warmupTimers, prewarmTimers,
26
+ flushConfigSave, flushBackendSave, flushOutputStyleSave,
27
+ withTeardownDeadline, closePatchRuntimeIfLoaded,
28
+ createCurrentSession, refreshRouteEffort,
29
+ invalidateContextStatusCache, invalidatePreSessionToolSurface,
30
+ applyResolvedCwd, resolveRoute, applyDeferredToolSurface, standaloneTools,
31
+ } = deps;
32
+ return {
33
+ async close(reason = 'cli-exit', options = {}) {
34
+ const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
35
+ setCloseRequested(true);
36
+ // SessionEnd: bridge teardown to the standard hook bus. reason mapped to
37
+ // standard values ('clear'/'exit' where applicable, else 'other'). Short
38
+ // await guard so a slow hook cannot wedge teardown; best-effort.
39
+ try {
40
+ const rl = String(reason || '').toLowerCase();
41
+ const endReason = /clear/.test(rl) ? 'clear'
42
+ : /exit|quit|cli-exit|shutdown|sigint|sigterm/.test(rl) ? 'exit'
43
+ : 'other';
44
+ const session = getSession();
45
+ if (session?.id) {
46
+ await withTeardownDeadline(
47
+ Promise.resolve(hooks.dispatch('SessionEnd', hookCommonPayload({ session_id: session.id, reason: endReason }))).catch(() => {}),
48
+ 300,
49
+ undefined,
50
+ );
51
+ }
52
+ } catch { /* best-effort: SessionEnd hook must never wedge teardown */ }
53
+ // Persist any change that is still sitting in the debounce window so a
54
+ // toggle made right before exit is not lost. Synchronous + best-effort:
55
+ // teardown must continue even if the final write fails.
56
+ try { flushConfigSave(); } catch {}
57
+ try { flushBackendSave(); } catch {}
58
+ try { flushOutputStyleSave(); } catch {}
59
+ if (prewarmTimers.channelStartTimer) {
60
+ clearTimeout(prewarmTimers.channelStartTimer);
61
+ prewarmTimers.channelStartTimer = null;
62
+ }
63
+ for (const timerKey of [
64
+ 'providerSetupWarmupTimer',
65
+ 'providerWarmupTimer',
66
+ 'providerModelWarmupTimer',
67
+ 'modelCatalogWarmupTimer',
68
+ ]) {
69
+ if (warmupTimers[timerKey]) {
70
+ clearTimeout(warmupTimers[timerKey]);
71
+ warmupTimers[timerKey] = null;
72
+ }
73
+ }
74
+ if (prewarmTimers.codeGraphPrewarmTimer) {
75
+ clearTimeout(prewarmTimers.codeGraphPrewarmTimer);
76
+ prewarmTimers.codeGraphPrewarmTimer = null;
77
+ }
78
+ for (const timerKey of ['statuslineUsageWarmupTimer', 'statuslineUsageRefreshTimer']) {
79
+ if (warmupTimers[timerKey]) {
80
+ clearTimeout(warmupTimers[timerKey]);
81
+ warmupTimers[timerKey] = null;
82
+ }
83
+ }
84
+ try { cancelBackgroundTasks({ reason, notify: false }); } catch {}
85
+ const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
86
+ try { agentTool.closeAll(reason); } catch {}
87
+ let mcpStop = null;
88
+ try { mcpStop = mcpClient.disconnectAll?.(); } catch {}
89
+ const openaiWsStop = globalThis.__mixdogOpenaiWsRuntimeLoaded === true
90
+ ? import('../runtime/agent/orchestrator/providers/openai-oauth-ws.mjs')
91
+ .then((mod) => mod?.drainOpenaiWsPool?.(reason))
92
+ .catch(() => {})
93
+ : null;
94
+ const patchStop = closePatchRuntimeIfLoaded(detach ? { waitForExit: false } : undefined);
95
+ const memoryModPromise = getMemoryModPromise();
96
+ const memoryStop = memoryModPromise
97
+ ? memoryModPromise
98
+ .then((mod) => (typeof mod?.stop === 'function' ? mod.stop() : null))
99
+ .catch(() => {})
100
+ .finally(() => {
101
+ setMemoryModPromise(null);
102
+ })
103
+ : null;
104
+ let ok = false;
105
+ const session = getSession();
106
+ if (session?.id) {
107
+ statusRoutes?.clearGatewaySessionRoute?.(session.id);
108
+ // Bug fix: runtime stop/exit (TUI Ctrl-C, process exit) previously
109
+ // always tombstoned the current session, so a session you were
110
+ // mid-conversation in vanished from the Resume list the instant you
111
+ // quit and was hard-deleted by the 24h tombstone sweep. Only
112
+ // tombstone truly-empty scratch sessions; non-empty sessions must
113
+ // survive exit resumable.
114
+ // liveTurnMessages holds the in-flight user prompt until turn
115
+ // commit — an active first-turn ask has its user message there,
116
+ // not yet in session.messages, so it must also be checked or a
117
+ // first-turn exit could still burn a real session.
118
+ const tombstone = !hasUserConversationMessage(session.messages)
119
+ && !hasUserConversationMessage(session.liveTurnMessages);
120
+ ok = mgr.closeSession(session.id, reason, { tombstone });
121
+ setSession(null);
122
+ }
123
+ const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
124
+ ? import('../runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs')
125
+ .then((mod) => mod?.shutdownShellJobs?.(reason, { sync: !detach }))
126
+ .catch(() => {})
127
+ : null;
128
+ const bashSessionsStop = globalThis.__mixdogBashSessionRuntimeLoaded === true
129
+ ? import('../runtime/agent/orchestrator/tools/bash-session.mjs')
130
+ .then((mod) => mod?.shutdownBashSessions?.(reason))
131
+ .catch(() => {})
132
+ : null;
133
+ if (detach) {
134
+ try { await withTeardownDeadline(channelStop, 300, false); } catch {}
135
+ try { await withTeardownDeadline(shellJobsStop, 300, false); } catch {}
136
+ try { await withTeardownDeadline(bashSessionsStop, 300, false); } catch {}
137
+ try { await withTeardownDeadline(memoryStop, 1500, false); } catch {}
138
+ for (const stop of [mcpStop, openaiWsStop, patchStop]) {
139
+ Promise.resolve(stop).catch(() => {});
140
+ }
141
+ return ok;
142
+ }
143
+ await Promise.allSettled([
144
+ withTeardownDeadline(channelStop, 5500, false),
145
+ withTeardownDeadline(mcpStop, 1500, false),
146
+ withTeardownDeadline(openaiWsStop, 1500, false),
147
+ withTeardownDeadline(patchStop, 1500, false),
148
+ withTeardownDeadline(memoryStop, 5500, false),
149
+ withTeardownDeadline(shellJobsStop, 1500, false),
150
+ withTeardownDeadline(bashSessionsStop, 1500, false),
151
+ ]);
152
+ return ok;
153
+ },
154
+ abort(reason = 'cli-abort') {
155
+ const session = getSession();
156
+ if (!session?.id) return false;
157
+ return mgr.abortSessionTurn(session.id, reason);
158
+ },
159
+ listSessions() {
160
+ return mgr.listSessions({}).map(s => {
161
+ const owner = clean(s.owner || 'user').toLowerCase();
162
+ if (owner && !['cli', 'user', 'mixdog', 'legacy'].includes(owner)) return null;
163
+ const sourceType = clean(s.sourceType || '').toLowerCase();
164
+ const sourceName = clean(s.sourceName || '').toLowerCase();
165
+ const agent = clean(s.agent || '').toLowerCase();
166
+ const leadish = agent === 'lead'
167
+ || sourceType === 'lead'
168
+ || (sourceType === 'cli')
169
+ || (!sourceType && !sourceName && !isAgentOwner(owner));
170
+ if (!leadish) return null;
171
+ let preview = cleanSessionPreview(s.preview || '');
172
+ let messageCount = Math.max(0, Number(s.messageCount) || 0);
173
+ if (!preview && Array.isArray(s.messages)) {
174
+ const msgs = s.messages || [];
175
+ const userPreviews = msgs
176
+ .filter(m => m && m.role === 'user')
177
+ .map(m => cleanSessionPreview(sessionMessageText(m.content)))
178
+ .filter(text => !isSessionPreviewNoise(text));
179
+ preview = userPreviews[userPreviews.length - 1] || userPreviews[0] || '';
180
+ messageCount = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant')).length;
181
+ }
182
+ if (!preview && messageCount === 0) return null;
183
+ return {
184
+ id: s.id,
185
+ updatedAt: s.updatedAt,
186
+ cwd: s.cwd || '',
187
+ model: s.model,
188
+ provider: s.provider,
189
+ messageCount,
190
+ preview,
191
+ };
192
+ }).filter(Boolean);
193
+ },
194
+ async newSession() {
195
+ const session = getSession();
196
+ if (session?.id) {
197
+ const tombstone = !hasUserConversationMessage(session.messages)
198
+ && !hasUserConversationMessage(session.liveTurnMessages);
199
+ mgr.closeSession(session.id, 'cli-new', { tombstone });
200
+ setSession(null);
201
+ }
202
+ invalidateContextStatusCache();
203
+ await createCurrentSession();
204
+ return getSession().id;
205
+ },
206
+ async resume(id) {
207
+ const prev = getSession();
208
+ const previousId = prev?.id || null;
209
+ const previousMessages = prev?.messages || null;
210
+ const previousLive = prev?.liveTurnMessages || null;
211
+ const resumed = await mgr.resumeSession(id, toolSpecForMode(getMode()));
212
+ if (!resumed) return null;
213
+ if (previousId && previousId !== resumed.id) {
214
+ statusRoutes?.clearGatewaySessionRoute?.(previousId);
215
+ const tombstone = !hasUserConversationMessage(previousMessages)
216
+ && !hasUserConversationMessage(previousLive);
217
+ mgr.closeSession(previousId, 'cli-resume', { tombstone });
218
+ }
219
+ setSession(resumed);
220
+ applyResolvedCwd(resumed.cwd || getCurrentCwd(), { markRefresh: false });
221
+ const route = getRoute();
222
+ const resumeEffort = hasOwn(route, 'effort') ? route.effort : resumed.effort;
223
+ setRoute(resolveRoute(getConfig(), { provider: resumed.provider, model: resumed.model, effort: resumeEffort }));
224
+ await refreshRouteEffort();
225
+ const session = getSession();
226
+ session.effort = getRoute().effectiveEffort || null;
227
+ session.cwd = getCurrentCwd();
228
+ applyDeferredToolSurface(session, deferredSurfaceModeForLead(getMode()), standaloneTools, { provider: getRoute().provider });
229
+ invalidatePreSessionToolSurface();
230
+ invalidateContextStatusCache();
231
+ setSessionNeedsCwdRefresh(false);
232
+ writeStatuslineRoute(statusRoutes, session, getRoute());
233
+ return {
234
+ id: resumed.id,
235
+ messages: resumed.messages || [],
236
+ cwd: getCurrentCwd(),
237
+ provider: resumed.provider,
238
+ model: resumed.model,
239
+ };
240
+ },
241
+ };
242
+ }
@@ -0,0 +1,198 @@
1
+ import { clean, hasOwn } from './session-text.mjs';
2
+ import { coerceEffortFor, normalizeEffortInput } from './effort.mjs';
3
+ import { fastCapableFor, saveModelSettings } from './model-capabilities.mjs';
4
+ import {
5
+ ensureProviderEnabled,
6
+ validateRequestedModelSelector,
7
+ findPreset,
8
+ modelMetaLooksResolved,
9
+ } from './config-helpers.mjs';
10
+ import { getModelMetadataSync } from '../runtime/agent/orchestrator/providers/model-catalog.mjs';
11
+ import {
12
+ workflowPresetId,
13
+ normalizeSearchRouteConfig,
14
+ isDefaultSearchRouteConfig,
15
+ isSearchCapableProvider,
16
+ SEARCH_DEFAULT_PROVIDER,
17
+ SEARCH_DEFAULT_MODEL,
18
+ } from './workflow.mjs';
19
+ import { writeStatuslineRoute } from './statusline-route.mjs';
20
+
21
+ // Model/route/search-route selection + mutation surface. Extracted verbatim from
22
+ // the runtime API object; stateless helpers are imported directly and the
23
+ // runtime injects live getters/setters for the mutable config/route/searchRoute/
24
+ // session locals plus the closure callbacks (config adopt/save, effort refresh,
25
+ // provider registry, statusline).
26
+ export function createModelRouteApi(deps) {
27
+ const {
28
+ getConfig, getRoute, setRouteState, getSession, setSession,
29
+ getConfigHasSecrets, getSearchRouteState, setSearchRouteState,
30
+ cfgMod, reg, mgr, statusRoutes,
31
+ resolveRoute, searchCapableFor, lookupModelMeta,
32
+ adoptConfig, saveConfigAndAdopt, ensureFullConfig,
33
+ persistLeadRoute, refreshRouteEffort,
34
+ refreshStatuslineUsageSnapshot, scheduleStatuslineUsageRefresh,
35
+ invalidateContextStatusCache, invalidateProviderCaches,
36
+ collectSearchProviderModels,
37
+ } = deps;
38
+ return {
39
+ getSearchRoute() {
40
+ const sr = normalizeSearchRouteConfig(getConfig().searchRoute) || normalizeSearchRouteConfig(getSearchRouteState());
41
+ setSearchRouteState(sr);
42
+ return sr;
43
+ },
44
+ async listSearchModels(options = {}) {
45
+ return await collectSearchProviderModels({ force: options.force === true || options.refresh === true });
46
+ },
47
+ async setSearchRoute(next) {
48
+ const searchRoute = getSearchRouteState();
49
+ let selectedRoute = normalizeSearchRouteConfig(next);
50
+ if (!selectedRoute && next?.model && searchRoute?.provider) {
51
+ selectedRoute = normalizeSearchRouteConfig({ ...next, provider: searchRoute.provider });
52
+ }
53
+ if (!selectedRoute) throw new Error('search route requires provider and model');
54
+ if (isDefaultSearchRouteConfig(selectedRoute)) {
55
+ ensureFullConfig();
56
+ const routeToSave = normalizeSearchRouteConfig({
57
+ provider: SEARCH_DEFAULT_PROVIDER,
58
+ model: SEARCH_DEFAULT_MODEL,
59
+ ...(selectedRoute.toolType ? { toolType: selectedRoute.toolType } : {}),
60
+ });
61
+ const nextConfig = { ...getConfig() };
62
+ nextConfig.searchRoute = routeToSave;
63
+ saveConfigAndAdopt(nextConfig);
64
+ const sr = normalizeSearchRouteConfig(getConfig().searchRoute);
65
+ setSearchRouteState(sr);
66
+ invalidateProviderCaches();
67
+ return sr;
68
+ }
69
+ if (!isSearchCapableProvider(selectedRoute.provider)) {
70
+ throw new Error(`provider "${selectedRoute.provider}" does not support Mixdog native search`);
71
+ }
72
+ ensureFullConfig();
73
+ await reg.initProviders(ensureProviderEnabled(getConfig(), selectedRoute.provider));
74
+ const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
75
+ if (!searchCapableFor(selectedRoute.provider, modelMeta)) {
76
+ throw new Error(`model "${selectedRoute.model}" is not marked as web-search capable`);
77
+ }
78
+ const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
79
+ const effort = coerceEffortFor(selectedRoute.provider, modelMeta, selectedRoute.effort);
80
+ selectedRoute = {
81
+ ...selectedRoute,
82
+ ...(effort ? { effort } : {}),
83
+ fast: fastCapable ? selectedRoute.fast === true : false,
84
+ };
85
+ adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: getConfig() }), { hasSecrets: getConfigHasSecrets() });
86
+ const routeToSave = normalizeSearchRouteConfig(selectedRoute);
87
+ const nextConfig = { ...getConfig() };
88
+ nextConfig.searchRoute = routeToSave;
89
+ saveConfigAndAdopt(nextConfig);
90
+ const sr = normalizeSearchRouteConfig(getConfig().searchRoute);
91
+ setSearchRouteState(sr);
92
+ invalidateProviderCaches();
93
+ return sr;
94
+ },
95
+ async setRoute(next, options = {}) {
96
+ // Model/provider changes take effect on the NEXT session only — never
97
+ // rewrite a running session's provider/model in place (provider-keyed
98
+ // prompt cache). `route` still updates immediately for the next session.
99
+ const applyToCurrentSession = options?.applyToCurrentSession === true;
100
+ const requested = { ...(next || {}) };
101
+ validateRequestedModelSelector(getConfig(), requested);
102
+ const providerExplicitlyRequested = clean(next?.provider) !== '';
103
+ if (requested.effort === undefined && !requested.provider && !requested.model && hasOwn(getRoute(), 'effort')) {
104
+ requested.effort = getRoute().effort;
105
+ }
106
+ if (!requested.provider && requested.model && !findPreset(getConfig(), requested.model)) {
107
+ requested.provider = getRoute().provider;
108
+ }
109
+ let selectedRoute = resolveRoute(getConfig(), requested);
110
+ await reg.initProviders(ensureProviderEnabled(getConfig(), selectedRoute.provider));
111
+ const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
112
+ if (!providerExplicitlyRequested
113
+ && !selectedRoute.preset
114
+ && !modelMetaLooksResolved(modelMeta)
115
+ && !getModelMetadataSync(selectedRoute.model, selectedRoute.provider)) {
116
+ throw new Error(`unknown model: ${selectedRoute.provider}/${selectedRoute.model}`);
117
+ }
118
+ const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
119
+ selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
120
+ adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: getConfig() }), { hasSecrets: getConfigHasSecrets() });
121
+ const leadRoute = persistLeadRoute(selectedRoute);
122
+ setRouteState(resolveRoute(getConfig(), leadRoute
123
+ ? { model: workflowPresetId('lead') }
124
+ : selectedRoute));
125
+ await refreshRouteEffort(modelMeta);
126
+ refreshStatuslineUsageSnapshot(getRoute());
127
+ scheduleStatuslineUsageRefresh();
128
+ if (!applyToCurrentSession) {
129
+ return getRoute();
130
+ }
131
+ const session = getSession();
132
+ if (session) {
133
+ const route = getRoute();
134
+ const updated = mgr.updateSessionRoute?.(session.id, {
135
+ provider: route.provider,
136
+ model: route.model,
137
+ fast: route.fast === true,
138
+ effort: route.effectiveEffort || null,
139
+ });
140
+ if (updated) setSession(updated);
141
+ else {
142
+ session.provider = route.provider;
143
+ session.model = route.model;
144
+ session.fast = route.fast === true;
145
+ session.effort = route.effectiveEffort || null;
146
+ }
147
+ writeStatuslineRoute(statusRoutes, getSession(), route);
148
+ invalidateContextStatusCache();
149
+ }
150
+ return getRoute();
151
+ },
152
+ async setFast(value) {
153
+ const enabled = value === true;
154
+ const modelMeta = await lookupModelMeta(getRoute().provider, getRoute().model);
155
+ const fastCapable = fastCapableFor(getRoute().provider, modelMeta);
156
+ if (enabled && !fastCapable) {
157
+ throw new Error(`fast mode is not available for ${getRoute().provider}/${getRoute().model}`);
158
+ }
159
+ setRouteState(resolveRoute(getConfig(), { provider: getRoute().provider, model: getRoute().model, effort: getRoute().effort, fast: fastCapable ? enabled : false }));
160
+ adoptConfig(saveModelSettings(cfgMod, getRoute(), { fastCapable, baseConfig: getConfig() }), { hasSecrets: getConfigHasSecrets() });
161
+ const leadRoute = persistLeadRoute(getRoute());
162
+ if (leadRoute) setRouteState(resolveRoute(getConfig(), { model: workflowPresetId('lead') }));
163
+ await refreshRouteEffort(modelMeta);
164
+ const session = getSession();
165
+ if (session) {
166
+ const route = getRoute();
167
+ session.fast = route.fast === true;
168
+ session.effort = route.effectiveEffort || null;
169
+ writeStatuslineRoute(statusRoutes, session, route);
170
+ invalidateContextStatusCache();
171
+ }
172
+ return getRoute().fast === true;
173
+ },
174
+ async toggleFast() {
175
+ return await this.setFast(!(getRoute().fast === true));
176
+ },
177
+ async setEffort(value) {
178
+ const normalized = normalizeEffortInput(value);
179
+ setRouteState({ ...getRoute(), effort: normalized });
180
+ const modelMeta = await lookupModelMeta(getRoute().provider, getRoute().model);
181
+ const fastCapable = fastCapableFor(getRoute().provider, modelMeta);
182
+ adoptConfig(saveModelSettings(cfgMod, getRoute(), { fastCapable, baseConfig: getConfig() }), { hasSecrets: getConfigHasSecrets() });
183
+ const leadRoute = persistLeadRoute(getRoute());
184
+ if (leadRoute) {
185
+ setRouteState(resolveRoute(getConfig(), { model: workflowPresetId('lead') }));
186
+ }
187
+ await refreshRouteEffort(modelMeta);
188
+ const session = getSession();
189
+ if (session) {
190
+ const route = getRoute();
191
+ session.effort = route.effectiveEffort || null;
192
+ writeStatuslineRoute(statusRoutes, session, route);
193
+ invalidateContextStatusCache();
194
+ }
195
+ return getRoute();
196
+ },
197
+ };
198
+ }
@@ -0,0 +1,135 @@
1
+ import {
2
+ beginOAuthProviderLogin,
3
+ forgetProviderAuth,
4
+ isKnownProvider,
5
+ loginOAuthProvider,
6
+ renderProviderStatus,
7
+ saveOpenAIUsageSessionKey,
8
+ saveOpenCodeGoUsageAuth,
9
+ loginOpenCodeGoUsage,
10
+ saveProviderApiKey,
11
+ setLocalProvider,
12
+ } from '../standalone/provider-admin.mjs';
13
+ import { clean } from './session-text.mjs';
14
+
15
+ // Provider auth / catalog / preset surface. Extracted verbatim from the runtime
16
+ // API object; the stateless admin helpers are imported directly and the runtime
17
+ // injects the closure-owned config/cache callbacks. `isKnownProvider` is
18
+ // re-imported here from provider-admin (same binding the runtime uses).
19
+ export function createProviderAuthApi({
20
+ cfgMod,
21
+ getConfig,
22
+ saveConfigAndAdopt,
23
+ displayConfig,
24
+ reloadFullConfig,
25
+ invalidateProviderCaches,
26
+ warmProviderModelCache,
27
+ cachedProviderSetup,
28
+ getUsageDashboard,
29
+ collectProviderModels,
30
+ }) {
31
+ return {
32
+ listProviders() {
33
+ return renderProviderStatus(displayConfig());
34
+ },
35
+ async getProviderSetup() {
36
+ return await cachedProviderSetup();
37
+ },
38
+ async getUsageDashboard(options = {}) {
39
+ return await getUsageDashboard(options);
40
+ },
41
+ async authenticateProvider(providerId, secret) {
42
+ const result = String(secret || '').trim()
43
+ ? saveProviderApiKey(cfgMod, providerId, secret)
44
+ : await loginOAuthProvider(cfgMod, providerId);
45
+ reloadFullConfig();
46
+ invalidateProviderCaches();
47
+ warmProviderModelCache();
48
+ return result;
49
+ },
50
+ async loginOAuthProvider(providerId) {
51
+ const result = await loginOAuthProvider(cfgMod, providerId);
52
+ reloadFullConfig();
53
+ invalidateProviderCaches();
54
+ warmProviderModelCache();
55
+ return result;
56
+ },
57
+ async beginOAuthProviderLogin(providerId) {
58
+ const result = await beginOAuthProviderLogin(cfgMod, providerId);
59
+ reloadFullConfig();
60
+ return {
61
+ ...result,
62
+ waitForCallback: result.waitForCallback?.then((completed) => {
63
+ reloadFullConfig();
64
+ if (completed) {
65
+ invalidateProviderCaches();
66
+ warmProviderModelCache();
67
+ }
68
+ return completed;
69
+ }),
70
+ completeCode: async (code) => {
71
+ const completed = await result.completeCode(code);
72
+ reloadFullConfig();
73
+ invalidateProviderCaches();
74
+ warmProviderModelCache();
75
+ return completed;
76
+ },
77
+ };
78
+ },
79
+ saveProviderApiKey(providerId, secret) {
80
+ const result = saveProviderApiKey(cfgMod, providerId, secret);
81
+ reloadFullConfig();
82
+ invalidateProviderCaches();
83
+ warmProviderModelCache();
84
+ return result;
85
+ },
86
+ saveOpenAIUsageSessionKey(secret) {
87
+ const result = saveOpenAIUsageSessionKey(cfgMod, secret);
88
+ reloadFullConfig();
89
+ invalidateProviderCaches();
90
+ return result;
91
+ },
92
+ saveOpenCodeGoUsageAuth(opts) {
93
+ const result = saveOpenCodeGoUsageAuth(cfgMod, opts);
94
+ reloadFullConfig();
95
+ invalidateProviderCaches();
96
+ return result;
97
+ },
98
+ async loginOpenCodeGoUsage() {
99
+ const result = await loginOpenCodeGoUsage(cfgMod);
100
+ reloadFullConfig();
101
+ invalidateProviderCaches();
102
+ return result;
103
+ },
104
+ setLocalProvider(providerId, opts) {
105
+ const result = setLocalProvider(cfgMod, providerId, opts);
106
+ reloadFullConfig();
107
+ invalidateProviderCaches();
108
+ warmProviderModelCache();
109
+ return result;
110
+ },
111
+ forgetProviderAuth(providerId) {
112
+ const result = forgetProviderAuth(cfgMod, providerId);
113
+ reloadFullConfig();
114
+ invalidateProviderCaches();
115
+ warmProviderModelCache();
116
+ return result;
117
+ },
118
+ listPresets() {
119
+ return cfgMod.listPresets(displayConfig());
120
+ },
121
+ async listProviderModels(options = {}) {
122
+ return await collectProviderModels({
123
+ force: options.force === true || options.refresh === true,
124
+ quick: options.quick === true,
125
+ });
126
+ },
127
+ async setDefaultProvider(provider) {
128
+ const requested = clean(provider);
129
+ if (!requested) throw new Error('provider is required');
130
+ if (!isKnownProvider(requested)) throw new Error(`unknown provider "${provider}"`);
131
+ saveConfigAndAdopt({ ...getConfig(), defaultProvider: requested });
132
+ return requested;
133
+ },
134
+ };
135
+ }