mixdog 0.9.84 → 0.9.86
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/README.md +7 -1
- package/package.json +3 -2
- package/scripts/channel-daemon-stub.mjs +2 -1
- package/src/defaults/agents.json +12 -0
- package/src/defaults/skills/setup/SKILL.md +44 -12
- package/src/help.mjs +40 -9
- package/src/lib/keychain-cjs.cjs +11 -1
- package/src/rules/agent/43-title-agent.md +22 -0
- package/src/rules/shared/01-tool.md +3 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +30 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +38 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +183 -41
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
- package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +44 -73
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
- package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +5 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +6 -8
- package/src/runtime/channels/lib/parent-bridge.mjs +27 -15
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
- package/src/runtime/channels/lib/worker-ipc.mjs +17 -4
- package/src/runtime/media/adapters/gemini-video.mjs +1 -1
- package/src/runtime/media/renditions.mjs +1 -1
- package/src/runtime/media/store.mjs +1 -1
- package/src/runtime/memory/index.mjs +14 -5
- package/src/runtime/memory/lib/agent-ipc.mjs +11 -12
- package/src/runtime/shared/atomic-file.mjs +76 -38
- package/src/runtime/shared/automation-attachments.mjs +3 -3
- package/src/runtime/shared/child-guardian.mjs +15 -5
- package/src/runtime/shared/child-spawn-gate.mjs +6 -12
- package/src/runtime/shared/err-text.mjs +1 -0
- package/src/runtime/shared/resource-admission.mjs +7 -3
- package/src/runtime/shared/safe-ipc-send.mjs +34 -0
- package/src/runtime/shared/turn-snapshot.mjs +0 -3
- package/src/session-runtime/lifecycle-api.mjs +43 -12
- package/src/session-runtime/prewarm.mjs +35 -0
- package/src/session-runtime/remote-control.mjs +8 -47
- package/src/session-runtime/remote-transcript.mjs +3 -7
- package/src/session-runtime/route-preparation.mjs +24 -0
- package/src/session-runtime/runtime-core.mjs +42 -86
- package/src/session-runtime/session-turn-api.mjs +9 -2
- package/src/session-runtime/workflow-agents-api.mjs +3 -0
- package/src/standalone/agent-shard/shard-child.mjs +300 -0
- package/src/standalone/agent-shard/shard-pool.mjs +442 -0
- package/src/standalone/agent-tool.mjs +222 -0
- package/src/standalone/channel-daemon-transport.mjs +126 -185
- package/src/standalone/channel-daemon.mjs +11 -11
- package/src/standalone/channel-worker.mjs +21 -19
- package/src/standalone/memory-runtime-proxy.mjs +98 -23
- package/src/tui/App.jsx +16 -9
- package/src/tui/app/channel-pickers.mjs +3 -8
- package/src/tui/app/route-pickers.mjs +117 -152
- package/src/tui/app/settings-picker.mjs +90 -89
- package/src/tui/app/slash-commands.mjs +7 -7
- package/src/tui/app/slash-dispatch.mjs +23 -58
- package/src/tui/app/theme-effort-pickers.mjs +3 -1
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
- package/src/tui/dist/index.mjs +358 -380
- package/src/tui/engine/live-share.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +24 -9
- package/src/tui/engine/session-flow.mjs +20 -10
- package/src/tui/engine/turn.mjs +28 -16
- package/src/tui/engine.mjs +24 -2
- package/src/tui/figures.mjs +0 -1
- package/src/ui/statusline-format.mjs +0 -1
- package/src/hooks/lib/permission-evaluator.cjs +0 -24
- package/src/lib/config-cjs.cjs +0 -61
- package/src/runtime/shared/launcher-control.mjs +0 -258
- package/src/runtime/shared/workspace-router.mjs +0 -259
|
@@ -49,6 +49,7 @@ export function createLifecycleApi(deps) {
|
|
|
49
49
|
createCurrentSession, refreshRouteEffort,
|
|
50
50
|
invalidateContextStatusCache, invalidatePreSessionToolSurface,
|
|
51
51
|
applyResolvedCwd, resolveRoute, applyDeferredToolSurface, getStandaloneTools,
|
|
52
|
+
beginRoutePreparation, clearRoutePreparation,
|
|
52
53
|
pushTranscriptRebind,
|
|
53
54
|
notificationListeners, remoteStateListeners, desktopSession,
|
|
54
55
|
} = deps;
|
|
@@ -158,6 +159,12 @@ export function createLifecycleApi(deps) {
|
|
|
158
159
|
return {
|
|
159
160
|
async close(reason = 'cli-exit', options = {}) {
|
|
160
161
|
const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
|
|
162
|
+
// Desktop multi-engine hosts dispose idle engines while other sessions
|
|
163
|
+
// keep working in the SAME process. The background-work registries
|
|
164
|
+
// (shared background tasks, shell jobs, bash sessions) are
|
|
165
|
+
// process-global, so a non-exit dispose must leave them running instead
|
|
166
|
+
// of reaping every session's jobs. CLI exit paths never set this.
|
|
167
|
+
const keepBackgroundWork = options?.keepBackgroundWork === true;
|
|
161
168
|
setCloseRequested(true);
|
|
162
169
|
// Self-update now stages in the background and swaps on the next clean
|
|
163
170
|
// launch (see staged-update.mjs) — nothing installs at shutdown. On a
|
|
@@ -218,7 +225,7 @@ export function createLifecycleApi(deps) {
|
|
|
218
225
|
warmupTimers[timerKey] = null;
|
|
219
226
|
}
|
|
220
227
|
}
|
|
221
|
-
try { cancelBackgroundTasks({ reason, notify: false }); } catch {}
|
|
228
|
+
try { if (!keepBackgroundWork) cancelBackgroundTasks({ reason, notify: false }); } catch {}
|
|
222
229
|
const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
|
|
223
230
|
try { agentTool.closeAll(reason); } catch {}
|
|
224
231
|
let mcpStop = null;
|
|
@@ -260,12 +267,12 @@ export function createLifecycleApi(deps) {
|
|
|
260
267
|
invalidateContextStatusCache();
|
|
261
268
|
notificationListeners?.clear?.();
|
|
262
269
|
remoteStateListeners?.clear?.();
|
|
263
|
-
const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
|
|
270
|
+
const shellJobsStop = !keepBackgroundWork && globalThis.__mixdogShellJobsRuntimeLoaded === true
|
|
264
271
|
? import('../runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs')
|
|
265
272
|
.then((mod) => mod?.shutdownShellJobs?.(reason, { sync: !detach }))
|
|
266
273
|
.catch(() => {})
|
|
267
274
|
: null;
|
|
268
|
-
const bashSessionsStop = globalThis.__mixdogBashSessionRuntimeLoaded === true
|
|
275
|
+
const bashSessionsStop = !keepBackgroundWork && globalThis.__mixdogBashSessionRuntimeLoaded === true
|
|
269
276
|
? import('../runtime/agent/orchestrator/tools/bash-session.mjs')
|
|
270
277
|
.then((mod) => mod?.shutdownBashSessions?.(reason))
|
|
271
278
|
.catch(() => {})
|
|
@@ -336,7 +343,8 @@ export function createLifecycleApi(deps) {
|
|
|
336
343
|
pushTranscriptRebind?.();
|
|
337
344
|
return true;
|
|
338
345
|
},
|
|
339
|
-
async switchContext({ cwd, desktopSession: nextDesktopSession } = {}) {
|
|
346
|
+
async switchContext({ cwd, desktopSession: nextDesktopSession, forResume = false } = {}) {
|
|
347
|
+
clearRoutePreparation?.();
|
|
340
348
|
const session = getSession();
|
|
341
349
|
if (session?.id) {
|
|
342
350
|
const cleanupReason = 'desktop-context-switch';
|
|
@@ -370,7 +378,7 @@ export function createLifecycleApi(deps) {
|
|
|
370
378
|
// that session's provider/model. A fresh desktop task or project must
|
|
371
379
|
// return to the configured Lead route instead of inheriting the route
|
|
372
380
|
// of whichever session happened to be open immediately beforehand.
|
|
373
|
-
if (typeof setRoute === 'function' && typeof getConfig === 'function'
|
|
381
|
+
if (!forResume && typeof setRoute === 'function' && typeof getConfig === 'function'
|
|
374
382
|
&& typeof resolveRoute === 'function') {
|
|
375
383
|
setRoute(resolveRoute(getConfig(), {}));
|
|
376
384
|
await refreshRouteEffort?.();
|
|
@@ -400,6 +408,7 @@ export function createLifecycleApi(deps) {
|
|
|
400
408
|
return mgr.prefetchSession?.(id, toolSpecForMode(getMode())) === true;
|
|
401
409
|
},
|
|
402
410
|
async resume(id) {
|
|
411
|
+
clearRoutePreparation?.();
|
|
403
412
|
const prev = getSession();
|
|
404
413
|
const previousId = prev?.id || null;
|
|
405
414
|
const previousMessages = prev?.messages || null;
|
|
@@ -424,22 +433,44 @@ export function createLifecycleApi(deps) {
|
|
|
424
433
|
}
|
|
425
434
|
setSession(resumed);
|
|
426
435
|
applyResolvedCwd(resolveResumeCwd(resumed, getCurrentCwd()), { markRefresh: false });
|
|
436
|
+
// Cwd application is synchronous even though MCP reconnect may continue
|
|
437
|
+
// in the background. Commit it before returning the resume transcript.
|
|
438
|
+
resumed.cwd = getCurrentCwd();
|
|
427
439
|
const route = getRoute();
|
|
428
440
|
// The resumed session's OWN effort wins. resolveRoute always returns an
|
|
429
441
|
// effort key, so this used to reinstate the effort of whichever session
|
|
430
442
|
// happened to be open before — a schedule/webhook session opened with a
|
|
431
443
|
// different effort than the one it actually ran with.
|
|
432
444
|
const resumeEffort = resumed.effort || (hasOwn(route, 'effort') ? route.effort : undefined);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
445
|
+
const resumedRoute = resolveRoute(
|
|
446
|
+
getConfig(),
|
|
447
|
+
{ provider: resumed.provider, model: resumed.model, effort: resumeEffort },
|
|
448
|
+
);
|
|
449
|
+
setRoute(resumedRoute);
|
|
450
|
+
const finishRoutePreparation = async () => {
|
|
451
|
+
const preparedRoute = await refreshRouteEffort(null, resumedRoute);
|
|
452
|
+
// Session or route changed while provider metadata was loading.
|
|
453
|
+
if (!preparedRoute || getSession() !== resumed || getRoute() !== preparedRoute) return false;
|
|
454
|
+
const activeSession = getSession();
|
|
455
|
+
activeSession.effort = getRoute().effectiveEffort || null;
|
|
456
|
+
activeSession.cwd = getCurrentCwd();
|
|
457
|
+
applyDeferredToolSurface(
|
|
458
|
+
activeSession,
|
|
459
|
+
deferredSurfaceModeForLead(getMode()),
|
|
460
|
+
getStandaloneTools(),
|
|
461
|
+
{ provider: getRoute().provider },
|
|
462
|
+
);
|
|
463
|
+
writeStatuslineRoute(statusRoutes, activeSession, getRoute());
|
|
464
|
+
return true;
|
|
465
|
+
};
|
|
466
|
+
if (typeof beginRoutePreparation === 'function') {
|
|
467
|
+
beginRoutePreparation(finishRoutePreparation);
|
|
468
|
+
} else {
|
|
469
|
+
await finishRoutePreparation();
|
|
470
|
+
}
|
|
439
471
|
invalidatePreSessionToolSurface();
|
|
440
472
|
invalidateContextStatusCache();
|
|
441
473
|
setSessionNeedsCwdRefresh(false);
|
|
442
|
-
writeStatuslineRoute(statusRoutes, session, getRoute());
|
|
443
474
|
// Session swapped to the resumed one: repoint the worker to the current
|
|
444
475
|
// transcript instead of waiting for the next inbound steal.
|
|
445
476
|
pushTranscriptRebind?.();
|
|
@@ -91,6 +91,40 @@ export function createPrewarmSchedulers({
|
|
|
91
91
|
timer.unref?.();
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Tool-runtime warmup: the per-call cold starts a session's FIRST tool call
|
|
95
|
+
// would otherwise pay — pwsh standby processes (~300ms each) and the o200k
|
|
96
|
+
// encoder + token-count worker (~100ms). Each part is independent and
|
|
97
|
+
// best-effort; failures only mark the boot profile.
|
|
98
|
+
function scheduleToolRuntimeWarmup(delayMs = 2500) {
|
|
99
|
+
if (envFlag('MIXDOG_DISABLE_TOOL_PREWARM')) {
|
|
100
|
+
bootProfile('tool-runtime:prewarm-skipped');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const timer = setTimeout(() => void (async () => {
|
|
104
|
+
if (isCloseRequested()) return;
|
|
105
|
+
try {
|
|
106
|
+
const { prewarmShellStandbys } = await import('../runtime/agent/orchestrator/tools/builtin/bash-tool.mjs');
|
|
107
|
+
bootProfile('tool-runtime:shell-standby', { warmed: prewarmShellStandbys() === true });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
bootProfile('tool-runtime:shell-standby-failed', { error: error?.message || String(error) });
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const { prewarmTokenEstimator } = await import('../runtime/agent/orchestrator/session/context-utils.mjs');
|
|
113
|
+
bootProfile('tool-runtime:token-estimator', { warmed: prewarmTokenEstimator() === true });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
bootProfile('tool-runtime:token-estimator-failed', { error: error?.message || String(error) });
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const { prewarmAgentShard } = await import('../standalone/agent-shard/shard-pool.mjs');
|
|
119
|
+
const warmed = await prewarmAgentShard();
|
|
120
|
+
if (warmed) bootProfile('tool-runtime:agent-shard', { warmed: true });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
bootProfile('tool-runtime:agent-shard-failed', { error: error?.message || String(error) });
|
|
123
|
+
}
|
|
124
|
+
})(), delayMs);
|
|
125
|
+
timer.unref?.();
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
function invokeChannelStart() {
|
|
95
129
|
if (state.channelStartPromise) return state.channelStartPromise;
|
|
96
130
|
const startedAt = performance.now();
|
|
@@ -142,6 +176,7 @@ export function createPrewarmSchedulers({
|
|
|
142
176
|
return {
|
|
143
177
|
scheduleCodeGraphPrewarm,
|
|
144
178
|
scheduleLeadSessionPrewarm,
|
|
179
|
+
scheduleToolRuntimeWarmup,
|
|
145
180
|
invokeChannelStart,
|
|
146
181
|
scheduleChannelStart,
|
|
147
182
|
};
|
|
@@ -10,8 +10,6 @@ export function createRemoteControl({
|
|
|
10
10
|
setRemoteEnabled,
|
|
11
11
|
getRemoteSessionId,
|
|
12
12
|
setRemoteSessionId,
|
|
13
|
-
setRemoteClaimPending,
|
|
14
|
-
isRemoteClaimPending,
|
|
15
13
|
isCloseRequested,
|
|
16
14
|
clearChannelStartTimer,
|
|
17
15
|
remoteTransitions,
|
|
@@ -28,22 +26,11 @@ export function createRemoteControl({
|
|
|
28
26
|
bootProfile,
|
|
29
27
|
envFlag,
|
|
30
28
|
}) {
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (intent === 'auto') {
|
|
37
|
-
// Auto-start attempts a claim-if-vacant: remoteEnabled flips only when the
|
|
38
|
-
// worker reports it actually acquired the seat, so a live owner elsewhere
|
|
39
|
-
// leaves this session non-remote.
|
|
40
|
-
setRemoteClaimPending(true);
|
|
41
|
-
} else {
|
|
42
|
-
setRemoteEnabled(true);
|
|
43
|
-
// Explicit toggle: the CURRENT session owns the relay (lazy create adopts
|
|
44
|
-
// inside ensureRemoteTranscriptWriter when no session exists yet).
|
|
45
|
-
setRemoteSessionId(getSession()?.id || null);
|
|
46
|
-
}
|
|
29
|
+
// Manual ON always targets the current session and explicitly overrides any
|
|
30
|
+
// previous manual owner. There is no auto/claim-if-vacant path.
|
|
31
|
+
function startRemote() {
|
|
32
|
+
setRemoteEnabled(true);
|
|
33
|
+
setRemoteSessionId(getSession()?.id || null);
|
|
47
34
|
// The worker forwards transcript ingests to the memory service, so its port
|
|
48
35
|
// must be live before the first ingest.
|
|
49
36
|
startMemoryDaemonEagerly({ getMemoryModule, bootProfile });
|
|
@@ -65,7 +52,6 @@ export function createRemoteControl({
|
|
|
65
52
|
// boots the worker, which runs headless when messaging is off.
|
|
66
53
|
if (!channelsEnabled() && !(await hasActiveAutomation().catch(() => false))) {
|
|
67
54
|
bootProfile('channels:start-disabled');
|
|
68
|
-
setRemoteClaimPending(false);
|
|
69
55
|
return isRemoteEnabled();
|
|
70
56
|
}
|
|
71
57
|
// A backend switch may still be writing; drain it before the worker
|
|
@@ -83,39 +69,16 @@ export function createRemoteControl({
|
|
|
83
69
|
ensureRemoteTranscriptWriter();
|
|
84
70
|
// Re-check after the awaits: stop/supersede or runtime close may have
|
|
85
71
|
// landed mid-chain — never boot for a session that turned remote off.
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
// The fork reads process.env synchronously, so set the inherited intent
|
|
91
|
-
// immediately before it and restore right after: the pollution window is
|
|
92
|
-
// this fork instead of the whole boot chain.
|
|
93
|
-
const previousIntent = process.env.MIXDOG_REMOTE_INTENT;
|
|
94
|
-
try {
|
|
95
|
-
process.env.MIXDOG_REMOTE_INTENT = intent;
|
|
96
|
-
await invokeChannelStart();
|
|
97
|
-
} finally {
|
|
98
|
-
if (previousIntent === undefined) delete process.env.MIXDOG_REMOTE_INTENT;
|
|
99
|
-
else process.env.MIXDOG_REMOTE_INTENT = previousIntent;
|
|
100
|
-
}
|
|
101
|
-
if ((!isRemoteEnabled() && !isRemoteClaimPending()) || isCloseRequested()) {
|
|
102
|
-
setRemoteClaimPending(false);
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
// Explicit claims are unconditional; auto-start dispatches the same
|
|
106
|
-
// activation as claim-if-vacant so it promotes an automation-only daemon
|
|
107
|
-
// without stealing a live remote owner.
|
|
72
|
+
if (!isRemoteEnabled() || isCloseRequested()) return false;
|
|
73
|
+
await invokeChannelStart();
|
|
74
|
+
if (!isRemoteEnabled() || isCloseRequested()) return false;
|
|
108
75
|
await channels.execute('activate_channel_bridge', {
|
|
109
76
|
active: true,
|
|
110
|
-
claimIfVacant: intent === 'auto',
|
|
111
77
|
sessionId: getSession()?.id || getRemoteSessionId() || null,
|
|
112
78
|
transcriptPath: getTranscriptPath(),
|
|
113
79
|
});
|
|
114
|
-
// The worker's acquire/supersede notification now owns the transition.
|
|
115
|
-
setRemoteClaimPending(false);
|
|
116
80
|
return isRemoteEnabled();
|
|
117
81
|
} catch (error) {
|
|
118
|
-
setRemoteClaimPending(false);
|
|
119
82
|
if (isRemoteEnabled()) {
|
|
120
83
|
setRemoteEnabled(false);
|
|
121
84
|
setRemoteSessionId(null);
|
|
@@ -135,7 +98,6 @@ export function createRemoteControl({
|
|
|
135
98
|
const releasingSessionId = getRemoteSessionId() || getSession()?.id || null;
|
|
136
99
|
setRemoteEnabled(false);
|
|
137
100
|
setRemoteSessionId(null);
|
|
138
|
-
setRemoteClaimPending(false);
|
|
139
101
|
clearChannelStartTimer();
|
|
140
102
|
return remoteTransitions.run(async () => {
|
|
141
103
|
try {
|
|
@@ -156,7 +118,6 @@ export function createRemoteControl({
|
|
|
156
118
|
function stopRemote(reason) {
|
|
157
119
|
setRemoteEnabled(false);
|
|
158
120
|
setRemoteSessionId(null);
|
|
159
|
-
setRemoteClaimPending(false);
|
|
160
121
|
clearChannelStartTimer();
|
|
161
122
|
channels.stop(reason || 'remote-disabled').catch(() => {});
|
|
162
123
|
return true;
|
|
@@ -17,7 +17,6 @@ export function createRemoteTranscript({
|
|
|
17
17
|
isCloseRequested,
|
|
18
18
|
channelsEnabled,
|
|
19
19
|
channels,
|
|
20
|
-
mgr,
|
|
21
20
|
bootProfile,
|
|
22
21
|
}) {
|
|
23
22
|
let writer = null;
|
|
@@ -36,9 +35,8 @@ export function createRemoteTranscript({
|
|
|
36
35
|
flushPendingTranscriptRebind,
|
|
37
36
|
};
|
|
38
37
|
|
|
39
|
-
// Bind (or refresh) the
|
|
40
|
-
//
|
|
41
|
-
// every other session, and a dead owner hands the relay over.
|
|
38
|
+
// Bind (or refresh) only the manually selected session. Session creation,
|
|
39
|
+
// close, or liveness never transfers Remote ownership.
|
|
42
40
|
function ensureRemoteTranscriptWriter() {
|
|
43
41
|
const session = getSession();
|
|
44
42
|
if (!isRemoteEnabled() || !session?.id) return false;
|
|
@@ -46,9 +44,7 @@ export function createRemoteTranscript({
|
|
|
46
44
|
if (!owner) {
|
|
47
45
|
setRemoteSessionId(session.id);
|
|
48
46
|
} else if (session.id !== owner) {
|
|
49
|
-
|
|
50
|
-
if (ownerSession && ownerSession.closed !== true && ownerSession.status !== 'closed') return false;
|
|
51
|
-
setRemoteSessionId(session.id);
|
|
47
|
+
return false;
|
|
52
48
|
}
|
|
53
49
|
const cwd = getCwd();
|
|
54
50
|
const key = `${session.id}\u0000${cwd}`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// One background route preparation can be associated with the currently
|
|
2
|
+
// resumed session. Resume/navigation reads do not wait for it; the next real
|
|
3
|
+
// turn does. Clearing replaces the visible gate without attempting to cancel
|
|
4
|
+
// provider initialization that may already be shared elsewhere.
|
|
5
|
+
export function createRoutePreparationGate({ onError } = {}) {
|
|
6
|
+
let pending = Promise.resolve();
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
clear() {
|
|
10
|
+
pending = Promise.resolve();
|
|
11
|
+
},
|
|
12
|
+
start(task) {
|
|
13
|
+
const operation = Promise.resolve().then(task);
|
|
14
|
+
pending = operation;
|
|
15
|
+
// Keep the original rejecting promise for waiters while still marking a
|
|
16
|
+
// background failure as observed until a submit reaches the same gate.
|
|
17
|
+
void operation.catch((error) => onError?.(error));
|
|
18
|
+
return operation;
|
|
19
|
+
},
|
|
20
|
+
wait() {
|
|
21
|
+
return pending;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -246,6 +246,7 @@ import { createToolSurface } from './tool-surface.mjs';
|
|
|
246
246
|
import { readRuntimeTunables } from './runtime-tunables.mjs';
|
|
247
247
|
import { createSessionTurnApi } from './session-turn-api.mjs';
|
|
248
248
|
import { providerInitCacheKey } from './provider-init-key.mjs';
|
|
249
|
+
import { createRoutePreparationGate } from './route-preparation.mjs';
|
|
249
250
|
import {
|
|
250
251
|
RUNTIME,
|
|
251
252
|
SEARCH_RUNTIME,
|
|
@@ -322,17 +323,8 @@ export async function createMixdogSessionRuntime({
|
|
|
322
323
|
let desktopSession = initialDesktopSession;
|
|
323
324
|
bootProfile('session-runtime:start', { provider, model, toolMode, cwd });
|
|
324
325
|
let remoteEnabled = remote === true;
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
// session yet. remoteEnabled flips only if the worker reports it acquired the
|
|
328
|
-
// seat (the 'acquired' notification). Lets the deferred start chain proceed
|
|
329
|
-
// past its remoteEnabled guards without prematurely showing this session as
|
|
330
|
-
// remote (single-holder: a live owner must not be stolen by autoStart).
|
|
331
|
-
let remoteClaimPending = false;
|
|
332
|
-
// SESSION-SCOPED remote (user decision): the session that toggled remote
|
|
333
|
-
// OWNS the channel relay. Other sessions' turns never rebind or relay; a
|
|
334
|
-
// dead/replaced owner (clear, delete) hands the seat to the next current
|
|
335
|
-
// session that asks.
|
|
326
|
+
// SESSION-SCOPED Remote is manual-only. Explicit ON selects/overrides; no
|
|
327
|
+
// registration, turn, session death, or process death transfers ownership.
|
|
336
328
|
let remoteSessionId = null;
|
|
337
329
|
// Desktop/TUI Remote toggles are desired-state transitions. Serialize the
|
|
338
330
|
// daemon deactivate/detach and reconnect/activate chains so an OFF that is
|
|
@@ -371,6 +363,11 @@ export async function createMixdogSessionRuntime({
|
|
|
371
363
|
})();
|
|
372
364
|
return keychainPrewarmWaitPromise;
|
|
373
365
|
}
|
|
366
|
+
const routePreparation = createRoutePreparationGate({
|
|
367
|
+
onError: (error) => bootProfile('route-preparation:failed', {
|
|
368
|
+
error: error?.message || String(error),
|
|
369
|
+
}),
|
|
370
|
+
});
|
|
374
371
|
|
|
375
372
|
const importsStartedAt = performance.now();
|
|
376
373
|
const [
|
|
@@ -744,28 +741,10 @@ export async function createMixdogSessionRuntime({
|
|
|
744
741
|
// newer remote session. Drop remote mode entirely on this session (no
|
|
745
742
|
// handover, no retry) and tell UI listeners so the indicator updates.
|
|
746
743
|
if (msg?.method === 'notifications/mixdog/remote') {
|
|
747
|
-
// Any acquire/supersede verdict resolves an in-flight auto claim attempt.
|
|
748
|
-
remoteClaimPending = false;
|
|
749
744
|
if (msg?.params?.state === 'superseded' && remoteEnabled) {
|
|
750
745
|
stopRemote('superseded-by-newer-remote-session');
|
|
751
746
|
emitRemoteStateChange(false, 'superseded');
|
|
752
747
|
}
|
|
753
|
-
// Symmetric acquire: the worker took the bridge seat (boot make-before-
|
|
754
|
-
// break or activate). Flip remote ON via the same side-state a user
|
|
755
|
-
// /remote toggles — remoteEnabled + transcript writer — but WITHOUT
|
|
756
|
-
// re-invoking channel start (the worker is already running; startRemote
|
|
757
|
-
// would re-fork/activate). Idempotent: no-op when already enabled.
|
|
758
|
-
if (msg?.params?.state === 'acquired' && !remoteEnabled) {
|
|
759
|
-
remoteEnabled = true;
|
|
760
|
-
if (!remoteSessionId) remoteSessionId = session?.id || null;
|
|
761
|
-
ensureRemoteTranscriptWriter();
|
|
762
|
-
// Auto-acquire: the worker restored yesterday's transcript from
|
|
763
|
-
// persisted status and we just created the CURRENT writer. Push the
|
|
764
|
-
// repoint now instead of waiting for the next inbound parent-chain
|
|
765
|
-
// steal, so outbound forwarding never tails a stale transcript.
|
|
766
|
-
pushTranscriptRebind();
|
|
767
|
-
emitRemoteStateChange(true, 'acquired');
|
|
768
|
-
}
|
|
769
748
|
return;
|
|
770
749
|
}
|
|
771
750
|
if (msg?.method !== 'notifications/claude/channel') return;
|
|
@@ -845,7 +824,11 @@ export async function createMixdogSessionRuntime({
|
|
|
845
824
|
if (name === 'recall' || name === 'memory' || name === 'search_memories') {
|
|
846
825
|
const memoryMod = await getMemoryModule();
|
|
847
826
|
if (!memoryMod?.handleToolCall) throw new Error('memory runtime is not available');
|
|
848
|
-
return await memoryMod.handleToolCall(
|
|
827
|
+
return await memoryMod.handleToolCall(
|
|
828
|
+
name,
|
|
829
|
+
memoryToolArgsForCaller(args, callerCwd),
|
|
830
|
+
callerCtx?.signal || session?.controller?.signal || null,
|
|
831
|
+
);
|
|
849
832
|
}
|
|
850
833
|
if (name === 'code_graph') {
|
|
851
834
|
const codeGraphMod = await getCodeGraphModule();
|
|
@@ -1170,12 +1153,18 @@ export async function createMixdogSessionRuntime({
|
|
|
1170
1153
|
return route;
|
|
1171
1154
|
}
|
|
1172
1155
|
|
|
1173
|
-
async function refreshRouteEffort(modelMetaOverride = null) {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1156
|
+
async function refreshRouteEffort(modelMetaOverride = null, expectedRoute = null) {
|
|
1157
|
+
const targetRoute = expectedRoute || route;
|
|
1158
|
+
await ensureProvidersReady(ensureProviderEnabled(config, targetRoute.provider));
|
|
1159
|
+
const modelMeta = modelMetaOverride || await lookupModelMeta(targetRoute.provider, targetRoute.model);
|
|
1160
|
+
// A rapid second resume/model change can replace the route while provider
|
|
1161
|
+
// metadata is loading. Never let the older completion overwrite it.
|
|
1162
|
+
if (expectedRoute && route !== expectedRoute) return null;
|
|
1163
|
+
const requested = hasOwn(targetRoute, 'effort')
|
|
1164
|
+
? targetRoute.effort
|
|
1165
|
+
: (targetRoute.preset?.effort || null);
|
|
1166
|
+
const effectiveEffort = coerceEffortFor(targetRoute.provider, modelMeta, requested);
|
|
1167
|
+
const fastCapable = fastCapableFor(targetRoute.provider, modelMeta);
|
|
1179
1168
|
// Carry the catalog display name onto the route so the statusline shows a
|
|
1180
1169
|
// human label (e.g. "Claude Fable 5") for preset-less direct models instead
|
|
1181
1170
|
// of the raw id. `name` is only trusted when it differs from the raw model
|
|
@@ -1183,11 +1172,11 @@ export async function createMixdogSessionRuntime({
|
|
|
1183
1172
|
// already-resolved label. Falls back to existing route.modelDisplay, then unset.
|
|
1184
1173
|
const metaName = clean(modelMeta?.name);
|
|
1185
1174
|
const modelDisplay = clean(modelMeta?.display) || clean(modelMeta?.displayName)
|
|
1186
|
-
|| (metaName && metaName !== clean(
|
|
1187
|
-
|| clean(
|
|
1175
|
+
|| (metaName && metaName !== clean(targetRoute.model) ? metaName : '')
|
|
1176
|
+
|| clean(targetRoute.modelDisplay);
|
|
1188
1177
|
route = {
|
|
1189
|
-
...
|
|
1190
|
-
fast: fastCapable ?
|
|
1178
|
+
...targetRoute,
|
|
1179
|
+
fast: fastCapable ? targetRoute.fast === true : false,
|
|
1191
1180
|
fastCapable,
|
|
1192
1181
|
effectiveEffort,
|
|
1193
1182
|
effortOptions: effortItemsFor(route.provider, modelMeta, effectiveEffort),
|
|
@@ -1393,6 +1382,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1393
1382
|
const {
|
|
1394
1383
|
scheduleCodeGraphPrewarm,
|
|
1395
1384
|
scheduleLeadSessionPrewarm,
|
|
1385
|
+
scheduleToolRuntimeWarmup,
|
|
1396
1386
|
invokeChannelStart,
|
|
1397
1387
|
scheduleChannelStart,
|
|
1398
1388
|
} = createPrewarmSchedulers({
|
|
@@ -1435,7 +1425,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1435
1425
|
isCloseRequested: () => closeRequested,
|
|
1436
1426
|
channelsEnabled,
|
|
1437
1427
|
channels,
|
|
1438
|
-
mgr,
|
|
1439
1428
|
bootProfile,
|
|
1440
1429
|
});
|
|
1441
1430
|
const ensureRemoteTranscriptWriter = () => remoteTranscript.ensureRemoteTranscriptWriter();
|
|
@@ -1450,8 +1439,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1450
1439
|
setRemoteEnabled: (next) => { remoteEnabled = next; },
|
|
1451
1440
|
getRemoteSessionId: () => remoteSessionId,
|
|
1452
1441
|
setRemoteSessionId: (next) => { remoteSessionId = next; },
|
|
1453
|
-
isRemoteClaimPending: () => remoteClaimPending,
|
|
1454
|
-
setRemoteClaimPending: (next) => { remoteClaimPending = next; },
|
|
1455
1442
|
isCloseRequested: () => closeRequested,
|
|
1456
1443
|
clearChannelStartTimer: () => {
|
|
1457
1444
|
if (prewarmTimers.channelStartTimer) {
|
|
@@ -1500,6 +1487,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1500
1487
|
codeGraphPrewarm: codeGraphPrewarmEnabled,
|
|
1501
1488
|
});
|
|
1502
1489
|
scheduleLeadSessionPrewarm();
|
|
1490
|
+
scheduleToolRuntimeWarmup();
|
|
1503
1491
|
// Lazy mode (default): skip the startup prewarm entirely; the first turn
|
|
1504
1492
|
// triggers it instead (see ask()). Eager mode keeps the old startup schedule.
|
|
1505
1493
|
if (!codeGraphPrewarmLazy) {
|
|
@@ -1519,32 +1507,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1519
1507
|
// Channels are opt-in: only boot the worker when this session started in (or
|
|
1520
1508
|
// was toggled into) remote mode. Non-remote sessions never contend for the
|
|
1521
1509
|
// channel; see startRemote()/stopRemote() and the `/remote` toggle.
|
|
1522
|
-
// `remote
|
|
1523
|
-
// remote claim at boot. Desktop sessions are explicit-only: opening the app
|
|
1524
|
-
// must never claim or spawn the channel bridge until its Remote button is
|
|
1525
|
-
// pressed, while the shared owner-state watcher still reflects a terminal
|
|
1526
|
-
// that already owns it.
|
|
1527
|
-
// The flag lives in the TOP-LEVEL `remote` section of mixdog-config.json
|
|
1528
|
-
// (sibling of agent/ui/channels), not inside the agent section that
|
|
1529
|
-
// cfgMod.loadConfig returns — read it via the shared whole-file reader.
|
|
1530
|
-
// `config?.remote?.autoStart` is kept for back-compat with agent-section
|
|
1531
|
-
// placement.
|
|
1532
|
-
// `remote` (from `mixdog --remote`) is EXPLICIT force-takeover. Config
|
|
1533
|
-
// `remote.autoStart` is AUTO: it must claim the seat ONLY if no live owner
|
|
1534
|
-
// exists (claim-if-vacant) and back off silently otherwise — so it does NOT
|
|
1535
|
-
// set remoteEnabled up front (that would assert remote before the claim is
|
|
1536
|
-
// known to have won). Track it as a separate deferred auto request; the
|
|
1537
|
-
// worker's acquire notification flips remoteEnabled if/when it wins the seat.
|
|
1538
|
-
let remoteAutoStartRequested = false;
|
|
1539
|
-
if (!remoteEnabled && !desktopSession) {
|
|
1540
|
-
try {
|
|
1541
|
-
if (config?.remote?.autoStart === true
|
|
1542
|
-
|| sharedCfgMod?.readSection?.('remote')?.autoStart === true) {
|
|
1543
|
-
remoteAutoStartRequested = true;
|
|
1544
|
-
}
|
|
1545
|
-
} catch { /* unreadable config never blocks boot */ }
|
|
1546
|
-
}
|
|
1547
|
-
// Boot-time remote start (autoStart or --remote) is DEFERRED past the TUI's
|
|
1510
|
+
// A manual `mixdog --remote` request is deferred past the TUI's
|
|
1548
1511
|
// first frame. startRemote() front-loads heavy work — memory daemon fork
|
|
1549
1512
|
// (PG + forced ONNX embed warmup in the child), eager session create, and
|
|
1550
1513
|
// the channel-worker fork — and running it inline here interleaves that
|
|
@@ -1553,22 +1516,18 @@ export async function createMixdogSessionRuntime({
|
|
|
1553
1516
|
// early /remote (startRemote clears it), stopRemote(), and close() all
|
|
1554
1517
|
// cancel it through the existing clearTimeout paths. Runtime /remote calls
|
|
1555
1518
|
// still start immediately (user-initiated, UI already painted).
|
|
1556
|
-
if (remoteEnabled
|
|
1557
|
-
|
|
1558
|
-
bootProfile('channels:autostart-deferred', { delayMs: remoteAutoStartDelayMs, intent: remoteStartIntent });
|
|
1519
|
+
if (remoteEnabled) {
|
|
1520
|
+
bootProfile('channels:manual-start-deferred', { delayMs: remoteAutoStartDelayMs });
|
|
1559
1521
|
prewarmTimers.channelStartTimer = setTimeout(() => {
|
|
1560
1522
|
prewarmTimers.channelStartTimer = null;
|
|
1561
|
-
if (closeRequested) return;
|
|
1562
|
-
|
|
1563
|
-
// Auto: always attempt (claim-if-vacant is safe when a live owner exists).
|
|
1564
|
-
if (remoteStartIntent === 'explicit' && !remoteEnabled) return;
|
|
1565
|
-
startRemote({ intent: remoteStartIntent });
|
|
1523
|
+
if (closeRequested || !remoteEnabled) return;
|
|
1524
|
+
startRemote();
|
|
1566
1525
|
}, remoteAutoStartDelayMs);
|
|
1567
1526
|
prewarmTimers.channelStartTimer.unref?.();
|
|
1568
1527
|
} else {
|
|
1569
1528
|
// Automation decoupling (user decision): enabled schedules/webhooks boot
|
|
1570
1529
|
// the worker on their own — no remote flag, no remote.autoStart, and no
|
|
1571
|
-
// messaging backend. A later explicit
|
|
1530
|
+
// messaging backend. A later explicit Remote ON promotes the same
|
|
1572
1531
|
// daemon without restarting schedules or webhooks.
|
|
1573
1532
|
prewarmTimers.channelStartTimer = setTimeout(() => {
|
|
1574
1533
|
prewarmTimers.channelStartTimer = null;
|
|
@@ -1703,6 +1662,8 @@ export async function createMixdogSessionRuntime({
|
|
|
1703
1662
|
applyResolvedCwd,
|
|
1704
1663
|
resolveRoute,
|
|
1705
1664
|
applyDeferredToolSurface,
|
|
1665
|
+
beginRoutePreparation: (task) => routePreparation.start(task),
|
|
1666
|
+
clearRoutePreparation: () => routePreparation.clear(),
|
|
1706
1667
|
// Live getter: cwd-refresh session rebuilds must re-evaluate the
|
|
1707
1668
|
// workflow's agent-tool gate, not reuse the boot-time array.
|
|
1708
1669
|
getStandaloneTools: modelStandaloneTools,
|
|
@@ -1842,6 +1803,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1842
1803
|
notificationListeners,
|
|
1843
1804
|
remoteStateListeners,
|
|
1844
1805
|
awaitInitialMcpConnect,
|
|
1806
|
+
awaitRoutePreparation: () => routePreparation.wait(),
|
|
1845
1807
|
});
|
|
1846
1808
|
|
|
1847
1809
|
return {
|
|
@@ -1940,15 +1902,9 @@ export async function createMixdogSessionRuntime({
|
|
|
1940
1902
|
getRemoteSessionId() {
|
|
1941
1903
|
return remoteSessionId;
|
|
1942
1904
|
},
|
|
1943
|
-
//
|
|
1944
|
-
// engine): rebind the transcript writer + forwarder without restarting
|
|
1945
|
-
// the worker. No-op when remote is off or no session exists.
|
|
1905
|
+
// Back-compat alias for an explicit manual ON in the current session.
|
|
1946
1906
|
claimRemoteForCurrentSession() {
|
|
1947
|
-
|
|
1948
|
-
remoteSessionId = session.id;
|
|
1949
|
-
ensureRemoteTranscriptWriter();
|
|
1950
|
-
pushTranscriptRebind();
|
|
1951
|
-
return true;
|
|
1907
|
+
return startRemote();
|
|
1952
1908
|
},
|
|
1953
1909
|
// Subscribe to non-user-initiated remote flips (seat superseded). Returns
|
|
1954
1910
|
// an unsubscribe function. TUI uses this to sync its Remote indicator and
|
|
@@ -43,7 +43,7 @@ export function createSessionTurnApi(deps) {
|
|
|
43
43
|
scheduleProviderWarmup, scheduleProviderModelWarmup, invalidateContextStatusCache,
|
|
44
44
|
agentTool, recreateCurrentSessionIfReady, invalidatePreSessionToolSurface,
|
|
45
45
|
activeToolSurface, applyResolvedCwd, resolveCwdPath, agentStatusState, notificationListeners,
|
|
46
|
-
awaitInitialMcpConnect,
|
|
46
|
+
awaitInitialMcpConnect, awaitRoutePreparation,
|
|
47
47
|
} = deps;
|
|
48
48
|
const enqueueRemoteAttachedPrompt = (prompt) => {
|
|
49
49
|
const attachedSession = getSession();
|
|
@@ -84,6 +84,10 @@ export function createSessionTurnApi(deps) {
|
|
|
84
84
|
session: attachedSession,
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
// Historical-session resume publishes its transcript immediately while
|
|
88
|
+
// provider/model metadata initializes in the background. Only the next
|
|
89
|
+
// actual turn waits, guaranteeing that it cannot run on a stale route.
|
|
90
|
+
await awaitRoutePreparation?.();
|
|
87
91
|
setActiveTurnCount(getActiveTurnCount() + 1);
|
|
88
92
|
// Lazy code-graph prewarm: kick off the build ONCE, on the first real
|
|
89
93
|
// turn, so a likely code lookup hits a warm cache.
|
|
@@ -115,7 +119,10 @@ export function createSessionTurnApi(deps) {
|
|
|
115
119
|
void invokeChannelStart()
|
|
116
120
|
.then(() => {
|
|
117
121
|
if (!getRemoteEnabled() || getCloseRequested()) return undefined;
|
|
118
|
-
|
|
122
|
+
// A turn may refresh the current owner's transcript, but it
|
|
123
|
+
// must never acquire/override Remote implicitly.
|
|
124
|
+
pushTranscriptRebind();
|
|
125
|
+
return undefined;
|
|
119
126
|
})
|
|
120
127
|
.catch((error) => bootProfile('channels:turn-rebind-failed', { error: error?.message || String(error) }));
|
|
121
128
|
}
|
|
@@ -165,6 +165,9 @@ export function createWorkflowAgentsApi(deps) {
|
|
|
165
165
|
description: workflow.description,
|
|
166
166
|
source: workflow.source,
|
|
167
167
|
active: workflow.id === active,
|
|
168
|
+
// Distinguishes "agents: (empty) = Lead only" from "no agents key =
|
|
169
|
+
// all fixed agents" for surfaces that render the pack's agent set.
|
|
170
|
+
agentsConfigured: workflow.agentsConfigured === true,
|
|
168
171
|
agents: workflow.agents,
|
|
169
172
|
}));
|
|
170
173
|
},
|