mixdog 0.9.85 → 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/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/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/tool-helpers.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +135 -9
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +4 -0
- 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/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/webhook/relay-tunnel.mjs +3 -3
- 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/shared/atomic-file.mjs +25 -14
- 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/resource-admission.mjs +7 -3
- package/src/runtime/shared/turn-snapshot.mjs +0 -3
- package/src/session-runtime/lifecycle-api.mjs +34 -9
- 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 +37 -85
- 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 +9 -10
- 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 +0 -45
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
- package/src/tui/dist/index.mjs +243 -323
- package/src/tui/engine/session-api-ext.mjs +24 -9
- package/src/tui/engine.mjs +7 -1
- 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
|
@@ -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;
|
|
@@ -1174,12 +1153,18 @@ export async function createMixdogSessionRuntime({
|
|
|
1174
1153
|
return route;
|
|
1175
1154
|
}
|
|
1176
1155
|
|
|
1177
|
-
async function refreshRouteEffort(modelMetaOverride = null) {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
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);
|
|
1183
1168
|
// Carry the catalog display name onto the route so the statusline shows a
|
|
1184
1169
|
// human label (e.g. "Claude Fable 5") for preset-less direct models instead
|
|
1185
1170
|
// of the raw id. `name` is only trusted when it differs from the raw model
|
|
@@ -1187,11 +1172,11 @@ export async function createMixdogSessionRuntime({
|
|
|
1187
1172
|
// already-resolved label. Falls back to existing route.modelDisplay, then unset.
|
|
1188
1173
|
const metaName = clean(modelMeta?.name);
|
|
1189
1174
|
const modelDisplay = clean(modelMeta?.display) || clean(modelMeta?.displayName)
|
|
1190
|
-
|| (metaName && metaName !== clean(
|
|
1191
|
-
|| clean(
|
|
1175
|
+
|| (metaName && metaName !== clean(targetRoute.model) ? metaName : '')
|
|
1176
|
+
|| clean(targetRoute.modelDisplay);
|
|
1192
1177
|
route = {
|
|
1193
|
-
...
|
|
1194
|
-
fast: fastCapable ?
|
|
1178
|
+
...targetRoute,
|
|
1179
|
+
fast: fastCapable ? targetRoute.fast === true : false,
|
|
1195
1180
|
fastCapable,
|
|
1196
1181
|
effectiveEffort,
|
|
1197
1182
|
effortOptions: effortItemsFor(route.provider, modelMeta, effectiveEffort),
|
|
@@ -1397,6 +1382,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1397
1382
|
const {
|
|
1398
1383
|
scheduleCodeGraphPrewarm,
|
|
1399
1384
|
scheduleLeadSessionPrewarm,
|
|
1385
|
+
scheduleToolRuntimeWarmup,
|
|
1400
1386
|
invokeChannelStart,
|
|
1401
1387
|
scheduleChannelStart,
|
|
1402
1388
|
} = createPrewarmSchedulers({
|
|
@@ -1439,7 +1425,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1439
1425
|
isCloseRequested: () => closeRequested,
|
|
1440
1426
|
channelsEnabled,
|
|
1441
1427
|
channels,
|
|
1442
|
-
mgr,
|
|
1443
1428
|
bootProfile,
|
|
1444
1429
|
});
|
|
1445
1430
|
const ensureRemoteTranscriptWriter = () => remoteTranscript.ensureRemoteTranscriptWriter();
|
|
@@ -1454,8 +1439,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1454
1439
|
setRemoteEnabled: (next) => { remoteEnabled = next; },
|
|
1455
1440
|
getRemoteSessionId: () => remoteSessionId,
|
|
1456
1441
|
setRemoteSessionId: (next) => { remoteSessionId = next; },
|
|
1457
|
-
isRemoteClaimPending: () => remoteClaimPending,
|
|
1458
|
-
setRemoteClaimPending: (next) => { remoteClaimPending = next; },
|
|
1459
1442
|
isCloseRequested: () => closeRequested,
|
|
1460
1443
|
clearChannelStartTimer: () => {
|
|
1461
1444
|
if (prewarmTimers.channelStartTimer) {
|
|
@@ -1504,6 +1487,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1504
1487
|
codeGraphPrewarm: codeGraphPrewarmEnabled,
|
|
1505
1488
|
});
|
|
1506
1489
|
scheduleLeadSessionPrewarm();
|
|
1490
|
+
scheduleToolRuntimeWarmup();
|
|
1507
1491
|
// Lazy mode (default): skip the startup prewarm entirely; the first turn
|
|
1508
1492
|
// triggers it instead (see ask()). Eager mode keeps the old startup schedule.
|
|
1509
1493
|
if (!codeGraphPrewarmLazy) {
|
|
@@ -1523,32 +1507,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1523
1507
|
// Channels are opt-in: only boot the worker when this session started in (or
|
|
1524
1508
|
// was toggled into) remote mode. Non-remote sessions never contend for the
|
|
1525
1509
|
// channel; see startRemote()/stopRemote() and the `/remote` toggle.
|
|
1526
|
-
// `remote
|
|
1527
|
-
// remote claim at boot. Desktop sessions are explicit-only: opening the app
|
|
1528
|
-
// must never claim or spawn the channel bridge until its Remote button is
|
|
1529
|
-
// pressed, while the shared owner-state watcher still reflects a terminal
|
|
1530
|
-
// that already owns it.
|
|
1531
|
-
// The flag lives in the TOP-LEVEL `remote` section of mixdog-config.json
|
|
1532
|
-
// (sibling of agent/ui/channels), not inside the agent section that
|
|
1533
|
-
// cfgMod.loadConfig returns — read it via the shared whole-file reader.
|
|
1534
|
-
// `config?.remote?.autoStart` is kept for back-compat with agent-section
|
|
1535
|
-
// placement.
|
|
1536
|
-
// `remote` (from `mixdog --remote`) is EXPLICIT force-takeover. Config
|
|
1537
|
-
// `remote.autoStart` is AUTO: it must claim the seat ONLY if no live owner
|
|
1538
|
-
// exists (claim-if-vacant) and back off silently otherwise — so it does NOT
|
|
1539
|
-
// set remoteEnabled up front (that would assert remote before the claim is
|
|
1540
|
-
// known to have won). Track it as a separate deferred auto request; the
|
|
1541
|
-
// worker's acquire notification flips remoteEnabled if/when it wins the seat.
|
|
1542
|
-
let remoteAutoStartRequested = false;
|
|
1543
|
-
if (!remoteEnabled && !desktopSession) {
|
|
1544
|
-
try {
|
|
1545
|
-
if (config?.remote?.autoStart === true
|
|
1546
|
-
|| sharedCfgMod?.readSection?.('remote')?.autoStart === true) {
|
|
1547
|
-
remoteAutoStartRequested = true;
|
|
1548
|
-
}
|
|
1549
|
-
} catch { /* unreadable config never blocks boot */ }
|
|
1550
|
-
}
|
|
1551
|
-
// 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
|
|
1552
1511
|
// first frame. startRemote() front-loads heavy work — memory daemon fork
|
|
1553
1512
|
// (PG + forced ONNX embed warmup in the child), eager session create, and
|
|
1554
1513
|
// the channel-worker fork — and running it inline here interleaves that
|
|
@@ -1557,22 +1516,18 @@ export async function createMixdogSessionRuntime({
|
|
|
1557
1516
|
// early /remote (startRemote clears it), stopRemote(), and close() all
|
|
1558
1517
|
// cancel it through the existing clearTimeout paths. Runtime /remote calls
|
|
1559
1518
|
// still start immediately (user-initiated, UI already painted).
|
|
1560
|
-
if (remoteEnabled
|
|
1561
|
-
|
|
1562
|
-
bootProfile('channels:autostart-deferred', { delayMs: remoteAutoStartDelayMs, intent: remoteStartIntent });
|
|
1519
|
+
if (remoteEnabled) {
|
|
1520
|
+
bootProfile('channels:manual-start-deferred', { delayMs: remoteAutoStartDelayMs });
|
|
1563
1521
|
prewarmTimers.channelStartTimer = setTimeout(() => {
|
|
1564
1522
|
prewarmTimers.channelStartTimer = null;
|
|
1565
|
-
if (closeRequested) return;
|
|
1566
|
-
|
|
1567
|
-
// Auto: always attempt (claim-if-vacant is safe when a live owner exists).
|
|
1568
|
-
if (remoteStartIntent === 'explicit' && !remoteEnabled) return;
|
|
1569
|
-
startRemote({ intent: remoteStartIntent });
|
|
1523
|
+
if (closeRequested || !remoteEnabled) return;
|
|
1524
|
+
startRemote();
|
|
1570
1525
|
}, remoteAutoStartDelayMs);
|
|
1571
1526
|
prewarmTimers.channelStartTimer.unref?.();
|
|
1572
1527
|
} else {
|
|
1573
1528
|
// Automation decoupling (user decision): enabled schedules/webhooks boot
|
|
1574
1529
|
// the worker on their own — no remote flag, no remote.autoStart, and no
|
|
1575
|
-
// messaging backend. A later explicit
|
|
1530
|
+
// messaging backend. A later explicit Remote ON promotes the same
|
|
1576
1531
|
// daemon without restarting schedules or webhooks.
|
|
1577
1532
|
prewarmTimers.channelStartTimer = setTimeout(() => {
|
|
1578
1533
|
prewarmTimers.channelStartTimer = null;
|
|
@@ -1707,6 +1662,8 @@ export async function createMixdogSessionRuntime({
|
|
|
1707
1662
|
applyResolvedCwd,
|
|
1708
1663
|
resolveRoute,
|
|
1709
1664
|
applyDeferredToolSurface,
|
|
1665
|
+
beginRoutePreparation: (task) => routePreparation.start(task),
|
|
1666
|
+
clearRoutePreparation: () => routePreparation.clear(),
|
|
1710
1667
|
// Live getter: cwd-refresh session rebuilds must re-evaluate the
|
|
1711
1668
|
// workflow's agent-tool gate, not reuse the boot-time array.
|
|
1712
1669
|
getStandaloneTools: modelStandaloneTools,
|
|
@@ -1846,6 +1803,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1846
1803
|
notificationListeners,
|
|
1847
1804
|
remoteStateListeners,
|
|
1848
1805
|
awaitInitialMcpConnect,
|
|
1806
|
+
awaitRoutePreparation: () => routePreparation.wait(),
|
|
1849
1807
|
});
|
|
1850
1808
|
|
|
1851
1809
|
return {
|
|
@@ -1944,15 +1902,9 @@ export async function createMixdogSessionRuntime({
|
|
|
1944
1902
|
getRemoteSessionId() {
|
|
1945
1903
|
return remoteSessionId;
|
|
1946
1904
|
},
|
|
1947
|
-
//
|
|
1948
|
-
// engine): rebind the transcript writer + forwarder without restarting
|
|
1949
|
-
// 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.
|
|
1950
1906
|
claimRemoteForCurrentSession() {
|
|
1951
|
-
|
|
1952
|
-
remoteSessionId = session.id;
|
|
1953
|
-
ensureRemoteTranscriptWriter();
|
|
1954
|
-
pushTranscriptRebind();
|
|
1955
|
-
return true;
|
|
1907
|
+
return startRemote();
|
|
1956
1908
|
},
|
|
1957
1909
|
// Subscribe to non-user-initiated remote flips (seat superseded). Returns
|
|
1958
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
|
},
|