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
|
@@ -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
|
+
}
|